套接字编程
套接字是一种网络API,用来开发网络程序。套接字接口提供一种进程间通信的方法,使得在相同或不同的主机上的进程能以相同的规范进行双向信息传送。
套接字类型
常用协议:
INET
ipv4INET6
ipv6
类型:SOCK_STREAM
:提供面向连接、可靠的数据传输服务。TCP协议支持。SOCK_DGRAM
:提供面向无连接的服务。UDP协议支持。SOCK_RAW
:原始套接字,常用于检测新的协议。套接字地址结构
套接字地址结构定义在头文件<netinet/in.h>
中IPv4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
uint8_t sin_len;//套接字地址结构长度
sa_family_t sin_family;//Internet地址族
in_port_t sin_port;//端口号
struct in_addr sin_addr;//结构体 成员存储IP地址
char sin_zero[8];//暂未使用,置0
};
IPv6
1 | typedef uint16_t in_port_t; |
通用套接字地址结构
定义在头文件<sys/socket.h>
中1
2
3
4
5struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
基本函数
字节排序函数
1 |
|
h:主机 n:网络 s:短整数 l:长整数
字节操纵函数
1 | /*BSD*/ |
bzero置0,bcopy拷贝,bcmp比较。1
2
3
4/*ANSI C*/
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes)
IP地址转换函数
1 |
|
inet_aton
函数将cp所指的字符串(点分十进制数串,如192.168.0.1)转换成32位的网络字节序二进制,并通过指针inp来存储。这个函数需要对字符串所指的地址进行有效性验证。但如果cp为空,函数仍然成功,但不存储任何结果。inet_addr
进行相同的转换,但不进行有效性验证,也就是说,所有232种可能的二进制值对inet_addr
函数都是有效的。inet_ntoa
函数将32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。但由于返回值所指向的串留在静态内存中,这意味着函数是不可重入的。
上述三个地址转换函数都只能处理IPv4协议,而不能处理IPv6地址。
inet_pton的实现(只支持ipv4)
1 | int inet_pton(int family, const char *strptr, void *addrptr) |
TCP套接字编程
服务器端
TCP服务器模板
1 | int main(void) |
TCP客户端模板
1 | /* include some header files */ |
基本套接字函数
socket
1 |
|
family
:协议族;type:套接字类型; protocol
:如调用者不想指定,一般为0 ,表示缺省,原始套接字除外。protocal
: 指明socket请求所使用的协议,IPPROTO_TCP
表示TCP协议,IPPROTO_UDP
表示UDP协议
创建socket代码
1
2
3
4
5
6
7
8
9
……
int sockfd;
//crteate socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) = = -1)
{
//handle exception
……
}
bind
1 |
|
该函数指明套接字将使用本地的哪一个协议的参数进行数据传送(IP地址和端口号)注意:协议地址addr
是通用地址。Len
是该地址结构(第二个参数)的长度。
setsockopt
1 |
|
Level
是选项所在的层及协议,有如下值:
- SOL_SOCKET (通用套接字 )
- IPPROTO_TCP(传输层,TCP协议)
- IPPROTO_IP(网际层,IP协议)
optname
是操作选项名optval
是一个指向变量的指针listen
1
2
3
int listen(int sockfd, int backlog)
返回:0-成功;-1-出错并置errno值;
仅被服务器调用。函数listen将未连接的套接字转化成被动套接字,指示内核应接受指向此套接字的连接请求;函数的第二个参数规定了内核为此套接字排队的最大连接个数;
connect
1 |
|
函数connect
激发TCP的三次握手过程。
|错误代码|原因|
|:–:|:—:|
|ETIMEDOUT|客户没有收到SYN分节响应|
|ECONNREFUSED|SYN响应为RST|
|EHOSTUNREACH或ENETUNREACH|ICMP错误,地址不可达|
accept
1 |
|
由TCP服务器调用,从已完成连接队列头返回下一个已完成连接。如果为空则进程进入睡眠状态。
close
1 |
|
关闭套接字。
如果套接字描述符访问计数在调用close后大于0(在多个进程共享同一个套接字的情况下),则不会引发TCP终止序列(即不会发送FIN分节);
shutdown
1 |
|
立即发送FIN分节。
read
1 |
|
接收缓冲区数据,返回接收到的字节数。
tcp协议收到FIN数据,返回0;
tcp协议收到RST数据,返回-1,同时errno为ECONNRESET;
进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。
write
1 |
|
向缓冲区发送数据,返回发送的字节数。
tcp协议接收到RST数据,返回-1,同时errno为ECONNRESET; ;
进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。
数据传输函数
send
1 |
|
flags
是传输控制标志,其值定义如下:
- 0:常规操作,如同write()函数
- MSG_OOB:发送带外数据(TCP紧急数据)。
- MSG_DONTROUTE:忽略底层协议的路由设置,只能将数据发送给与发送机处在同一个网络中的机器上。
recv
1
2
3
4
ssize_t recv(int fd, void *buf ,size_t len, int flags);
返回:大于0表示成功接收的数据长度;0: 对方已关闭,-1:出错。
flags
是传输控制标志,其值定义如下:
- 0:常规操作,如同read()函数;
- MSG_PEEK:只查看数据而不读出数据,后续读操作仍然能读出所查看的该数据;
- MSG_OOB:忽略常规数据,而只读带外数据;
- MSG_WAITALL:recv函数只有在将接收缓冲区填满后才返回。
域名解析函数——gethostbyname
1
2
3
struct hostent *gethostbyname(const char *hostname);
返回:非空指针-成功;空指针-出错,同时设置h_error
可解析IPv4和IPv6地址,既可接收域名也可接收点分十进制。
当hostname
为点分十进制时,函数不执行网络查询直接拷贝到结果。
读取文件函数
1 | char *fgets(char *buf, int num, FILE *fp); |
从fp
指向的文件读取一个长度为num-1的字符串,存入起始地址为buf
的空间。返回地址buf
,若遇文件结束或出错,返回NULL。
其中,fp
使用stdin
就是从标准输入读取数据
UDP套接字编程
服务器端
- 建立UDP套接字;
- 绑定套接字到特定地址;
- 等待并接收客户端信息;
- 处理客户端请求;
- 发送信息回客户端;
- 关闭套接字;
客户端
- 建立UDP套接字;
- 发送信息给服务器;
- 接收来自服务器的信息;
- 关闭套接字
服务器端模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(void)
{
int socketfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror(“Create socket failed.”);
exit(1);
}
/* Bind socket to address */
……
loop {
/* receive and process data from client */
……
/* send resuts to client */
}
close(sockfd); }

客户端模板
1 |
|
UDP数据传输函数
sendto
1 |
|
UDP是无连接协议,必须使用sendto
函数。指明目的地址。
flags是传输控制标志,其值定义如下:
0
:常规操作,如同write()函数;
MSG_OOB
:发送带外数据;
MSG_DONTROUTE
:忽略底层路由协议,直接发送。
recvfrom
1 |
|
指明源地址。flags是传输控制标志,其值定义如下:
0
:常规操作,如同read()函数;
MSG_PEEK
:只察看数据而不读出数据;
MSG_OOB
:忽略常规数据,而只读取带外数据;
注意复习实验内容和作业内容
多线程多进程并发服务器
Linux系统支持并发三种方式:进程,线程, I/O多路复用
按连接类型分为面向连接和无连接的服务器
按处理方式分为迭代和并发服务器
进程
基本概念
进程是一个动态实体,是独立的任务,拥有独立的地址空间、执行堆栈、文件描述符。进程是相互独立的,互不影响。可以通过IPC机制相互通信。
进程操作
创建新进程
1 |
|
终止进程
进程的终止存在两个可能:
- 父进程先于子进程终止(init进程领养)
- 子进程先于主进程终止
1 |
|
获取子进程终止信息
1 | #include<sys/types.h> |
如果没有终止的子进程,但是有一个或多个正在执行的子进程,则该函数将堵塞,直到有一个子进程终止或者wait被信号中断时,wait返回。
当调用该系统调用时,如果有一个子进程已经终止,则该系统调用立即返回,并释放子进程所有资源。
使用wait可能会留下僵尸进程1
2pid_t waitpid(pid_t pid, int *stat_loc, int options);
//返回:终止子进程的ID-成功;-1-出错;stat_loc存储子进程的终止状态;
pid
:
- -1:要求知道任何一个子进程的返回状态(等待第一个终止的子进程);
- >0:要求知道进程号为pid的子进程的状态;
- <-1:要求知道进程号为pid的绝对值的子进程的终止状态
options
:最常用的选项是WNOHANG,它通知内核在没有已终止进程时不要堵塞。
调用wait或waitpid函数时,正常情况下,可能会有以下几种情况:
- 阻塞(如果其所有子进程都还在运行);
- 获得子进程的终止状态并立即返回(如果一个子进程已终止,正等待父进程存取其终止状态);
- 出错立即返回(如果它没有任何子进程)
多进程并发服务器模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34int main(void)
{
int listenfd, connfd;
pid_t pid;
int BACKLOG = 5;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror(“Create socket failed.”);
exit(1);
}
bind(listenfd, …);
listen(listenfd, BACKLOG);
while(1) {
if ((connfd = accept(sockfd, NULL, NULL)) == -1) {
perror(“Accept error.”);
exit(1);
}
if((pid = fork() ) > 0){
/*parent process */
close(connfd);
…….
continue;
}
else if (pid == 0){
/*child process */
close(lisetenfd);
…….
exit(0);
}
else{
printf(“fork error\n”);
exit(1);
}
}
}
父子进程对套接字处理
产生新的子进程后,父进程要关闭连接套接字,子进程要关闭监听套接字。
原因:
- 节省系统资源。
- 避免父子进程同时对共享描述符进程操作造成错误。
- 为了正确关闭连接,需要父进程将不需要的已连接描述符关闭,子进程关闭不需要的监听描述符,使访问计数符在关闭时能置为0。
僵尸进程与孤儿进程
参考文章:https://www.cnblogs.com/Anker/p/3271773.html
僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait
或waitpid
获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
防止:
- 子进程正常或异常终止时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
- fork两次。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
线程
基本概念
线程是进程内的独立执行实体和调度单元,又称为“轻量级”进程(lightwight process);创建线程比进程快10~100倍。
线程操作
创建线程
1 |
|
tid
:线程的id标识,attr
:属性如优先级,是否是守护线程。func
:线程的执行函数。arg
:线程的调用参数。
常见的返回错误值:
- EAGAIN:超过了系统线程数目的限制。
- ENOMEN:没有足够的内存产生新的线程。
EINVAL:无效的属性attr值。
等待线程终止
1
2
3
int pthread_join(pthread_t tid, void **status);
// 返回:成功时为0;出错时为正的Exxx值,不设置error与
waitpid
函数类似,指定等待线程的ID。被等待线程必须是当前进程成员,且不是分离的线程和守护线程。分离线程
1
2
3
int pthread_detach(pthread_t tid)
返回:成功时为0;出错时为正Exxx值;
线程分为可联合的(joinable)和分离的(detached)。前者终止时,线程ID和终止状态将保留,直到另外一个线程调用pthread_join
。后者终止时释放所有资源。
此函数将指定线程设置为分离的。
获取ID
1 |
|
终止线程
1 |
|
指针status
执行线程的退出状态。
终止线程的三种方法:
- 调用
pthread_exit
- 启动线程的函数
pthread_create
的第三个参数,返回终止状态。 - 进程终止,线程随之终止。
一次执行
1
2
3#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))
// 成功返回0,否则返回错误码
如果本函数中,once_control
变量使用的初值为PTHREAD_ONCE_INIT
,可保证init_routine()
函数在本进程执行序列中仅执行一次。
LinuxThreads使用互斥锁和条件变量保证由pthread_once()
指定的函数执行且仅执行一次,而once_control
则表征是否执行过。如果once_control
的初值不是PTHREAD_ONCE_INIT
(LinuxThreads定义为0),pthread_once()
的行为就会不正常;
取消机制
1 | int pthread_cancel(pthread_t tid); |
线程通过向另外一个线程发送取消请求,接收到取消请求的线程根据其设置状态,作出:1)忽略该请求;2)立即终止自己;3) 延迟一段时间终止自己;
给新线程传递参数的方法
由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,下面参考如下几种方法:
- 传递参数的普通方法
- 通过指针传递参数
- 通过分配arg的空间来传递参数
- 还可以通过加锁等同步设施来实现传递参数
多线程并发服务器模板
1 | void *start_routine( void *arg); |
互斥锁
先看一段代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int myglobal=0;
void *thread_function(void *arg)
{
int i,j;
for ( i=0; i<5; i++)
{
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
}
return NULL;
}
int main(void)
{
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) )
{
printf("error creating thread.");
exit(1);
}
for ( i=0; i<5; i++)
{
myglobal=myglobal+1;
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) )
{
printf("error joining thread.");
exit(1);
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}
运行结果:
主线程和新线程都将myglobal加1,一共累加10次。结果应是10,为什么是5?
答案:因为没有解决同步问题,最后退出的线程值确定全局变量的值。
互斥锁用来保护线程代码中共享数据的完整性。
- 操作系统将保证同时只有一个线程能成功完成对一个互斥锁的加锁操作。
- 如果一个线程已经对某一互斥锁进行了加锁,其他线程只有等待该线程完成对这一互斥锁解锁后,才能完成加锁操作。
函数
1 | pthread_mutex_lock(pthread_mutex_t *mptr) |
mptr
:指向互斥锁的指针。
该函数接受一个指向互斥锁的指针作为参数并将其锁定。如果互斥锁已经被锁定,调用者将进入睡眠状态。函数返回时,将唤醒调用者。
如果互斥锁是静态分配的,就将mptr初始化为常值PTHREAD_MUTEX_INITIALIZER
。1
2pthread_mutex_unlock(pthread_mutex_t *mptr)
//返回:成功0.否则返回错误码
用于互斥锁解锁操作。
改进后代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int myglobal=0;
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg) {
int i,j;
for ( i=0; i<5; i++) {
pthread_mutex_lock(&mymutex);
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
pthread_mutex_unlock(&mymutex);
}
return NULL;
}
int main(void) {
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
for ( i=0; i<5; i++) {
pthread_mutex_lock(&mymutex);
myglobal=myglobal+1;
pthread_mutex_unlock(&mymutex);
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}
线程安全性编程
在多线程环境中,应避免使用静态变量。在linux环境中,用线程专用数据TSD取代静态变量。它类似于全局数据,只不过它是线程私有的,是以线程为界限的。
TSD是定义线程私有全局数据的唯一方法
每个TSD由进程内唯一的关键字(key)来标志,用来存取线程私有的数据。
函数
分配函数
1 |
|
该函数在进程内部分配一个标志TSD的关键字,关键字是进程内部唯一的,所有线程在创建时关键字值是NULL。每个进程只能调用一次。
key指向创建的关键字;destructor是一个可选的析构函数,用于每个线程终止时调用该析构函数。
绑定函数
1 |
|
为TSD关键字绑定一个与本线程相关的值。
获取值
1 | void * pthread_getspecific(pthread_key_t key); |
获得与调用线程相关的关键字所绑定的值。
删除函数
1 | int pthread_key_delete(pthread_key_t key); |
该函数删除进程内的TSD表示的关键字。该函数既不检查TSD是否有绑定值,也不调用该关键字的析构函数。
I/O复用
五个模型
阻塞I/O
非阻塞I/O
循环调用recvfrom,称为轮询,极为浪费CPU资源。当请求不成功会有错误返回。
I/O复用
调用select
或poll
,并在该函数上阻塞。select的好处在于可以等待多个描述字准备好。
信号驱动I/O
等待数据报到达时,可以不阻塞。
异步I/O
让内核启动操作,并在整个操作完成后通知。
异步i/o让与信号驱动i/o的区别是:后者是由内核通知我们何时可以启动一个i/o操作,而前者是由内核通知我们i/o操作何时完成。
比较
同步与异步
- 同步i/o操作引起请求进程阻塞,直到i/o操作完成;
- 异步i/o操作不引起请求进程阻塞;
只有最后一个异步I/O模型是异步I/Oselect函数
函数体
1
2
3
4
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
//返回:准备好描述字的总数量,0-超时,-1-出错,大于0-总的位数
指示内核等待多个事件中的任意一个发生,并仅在一个或多个时间发生或经过指定时间时才唤醒进程。timeval
结构体可以提供秒数和毫秒数成员。1
2
3
4struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
}
该函数有三种执行结果:
- 永远等待下去:仅在有一个或以上描述字准备好i/o才返回,为此,我们将
timeout
设置为空指针。 - 等待固定时间:在有一个描述字准备好时返回,但不超过由
timeout
参数指定的秒数和微秒数。 - 根本不等待,检查描述字后立即返回,这称为轮询。这种情况下,
timeout
必须指向结构timeval
,且定时器的值必须为0。参数说明
参数readset
,writeset
,execeptset
指定让内核测试读,写,异常条件的描述字。1
2
3
4void FD_ZERO(fd_set *fdset); /* clear all bits in fdset 所有位设0*/
void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset fd位设1*/
void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset fd位设0*/
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset 检测fd位是否为1*/
分配一个fd_set数据类型的描述字集,利用上面四个宏操作。
参数maxfdp1
指定被测试的描述字的个数,它是被测试的最大描述字加1。
套接字可读条件
- 套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区限度的当前值,对于TCP/UDP默认值为1;
- 套接字是一个监听套接字且已完成的连接数为非0。
- 如果对方tcp发送一个FIN(对方进程终止),套接字就变为可读且read返回0;
- 有一个套接字错误待处理。
套接字可写条件
- 套接字发送缓冲区的可用空间大于等于套接字发送缓冲区的限度的当前值;
- 套接字的写这一半关闭,对套接字的写将产生SIGPIPE信号;
- 有一个套接字错误待处理
套接字异常条件
- 套接口带外数据的到达;
- 控制状态信息的存在;
实现I/O多路复用步骤
- 清空描述符集合;
- 建立需要监视的描述符与描述符集合的联系;
- 调用select()函数;
- 检查所有需要监视的描述符,利用FD_ISSET宏判断是否已准备好;
- 对已准备好的描述符进行I/O操作。