TCP
服务器
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buf[MAXLINE];
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = accept(listenfd, (SA *) NULL,NULL);
str_echo(connfd);
close(connfd);
}
close(listenfd);
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0){
write(sockfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0){
printf("str_echo: read error");
exit(0);
}
}
客户端
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2){
printf("usage: tcpcli <IPaddress>");
exit(0);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL) {
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli: server terminated prematurely");
exit(0);
}
fputs(recvline, stdout);
}
}
问: listen函数干了些什么
- 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用
connect
发起连接的客户套接字。listen
函数把一个未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求。即调用listen
之后,套接字从CLOSED
状态转换为LISTEN
状态 - 内核会为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列:当监听套接字收到客户端发来的
SYN
之后,而服务器还没有和这个连接进行TCP三次握手的时候,这些套接字处理SYN_SEND
状态。 - 已完成连接队列:每个已经完成TCP三路握手过程的客户对应其中一项。这些套接字处于
ESTABLISHED
状态
- 未完成连接队列:当监听套接字收到客户端发来的
- 每当在未完成队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中,连接的创建机制是完全自动的,无需服务器插手
listen函数干了些什么
问: accpt函数干了什么
- accept由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接。如果已完成连接队列未空,那么进程将投入睡眠(假设套接字为默认阻塞方式)
问: 服务器启动但是客户端不启动是什么样子的呢?
- 服务器启动之后,调用
socket
、bind
、listen
和accept
,并且阻塞与accept
调用(此时还没有客户端启动)。在启动客户端之前,我们先调用nestat
程序来检查服务器监听套接字的状态
$ ps -a
PID TTY TIME CMD
6465 pts/1 00:00:00 ps
$ ./AAA &
[1] 6466
$ netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
从上面我们可以看出,有一个套接字处于LISTEN
状态(监听是由连接到这个套接字的所有请求,然后将这些请求放到未完成连接队列中,内核会自动完成这些请求的三次握手,完成三次握手之后,将之转移到已完成队列中):它有通配的本地IP地址(0 0.0.0.0
),本地端口为9877。这是一个监听套接字
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4134 3814 pts/1 Ss /bin/bash --rcfile /home/oc do_wait
6466 4134 pts/1 S ./AAA inet_csk_accept
从上面可以看出,AAA(监听套接字)的stat的状态是S,表示进程在为等待某些资源而睡眠。睡眠原因inet_csk_accept
表示等待客户端的连接请求( 此时程序阻塞在accept()
函数这里,accpet
的功能是从已连接队列中取出队首元素。而TCP三次握手是listen函数完成的)
问: 服务器先启动客户端后启动的现象
客户端会调用socket
和connect
,connect
会引发三路握手。
$ netstat -anp |grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 6466/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:54972 ESTABLISHED 6466/./AAA
tcp 0 0 127.0.0.1:54972 127.0.0.1:9877 ESTABLISHED 6512/./vvv
// 5826、5869为进程ID
从上图可以看出,与9877端口相关的进程有两个,共涉及到三个套接字
- 6466进程上有两个套接字,一个监听套接字(LISTEN表示等待连接),一个连接套接字(ESTABLISHED表示等待和127.0.0.1:54938上的另外一个连接套接字通信)
- 当三次握手完成之后,内核就会使用监听套接字的参数创建一个连接套接字,用于和客户端通信
- 6512进程上有一个套接字,连接套接字(当前正在使用127.0.0.1:54938端口,准备和127.0.0.1:9877上的套接字通信)
(1)观察客户端连接套接字:
ps -t pts/0 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND (刚刚的启动命令) WCHAN
6512 4020 pts/0 S+ ./vvv 127.0.0.1 n_tty_read
S+表示表示客户端正在睡眠(阻塞),而且是前台运行的。阻塞原因是因为正在等待终端读取(fget
函数)
(2)首先我们来看一下监听套接字有没有变化
之前的
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
6466 4134 pts/1 S ./AAA inet_csk_accept
现在的
$ $ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
6466 4134 pts/1 S ./AAA sk_wait_data
从上面可以看出,进程5826处于睡眠(阻塞)状态,但是现在阻塞的原因由inet_csk_accept
变成了sk_wait_data
inet_csk_accept
表示当前已完成连接队列是空的,因此阻塞等待客户端连接请求进入已完成连接队列中sk_wait_data
表示当超时或者网络事件发生则唤醒当前进程(read
函数)
(2.1) 从当前客户端的终端输入字符
输入字符之后,当前客户端还是阻塞等待终端输入的状态
$ ps -t pts/0 -o pid,ppid,tty,stat,args,wchan
6512 4020 pts/0 S+ ./vvv 127.0.0.1 n_tty_read
服务端还是阻塞在网络事件(有数据通信)或者超时事件
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
6466 4134 pts/1 S ./AAA sk_wait_data
connfd的连接并没有断开。
问: 为什么客户端与服务器可以不停通信,而不是发送一个消息就关闭呢?
<1> 我们先来分析客户端
从上面可以看出这个客户端只有在这个进程退出时才会关闭connfd。这里又有问题了
- 如果客户端主动关闭这个conndf(客户端进程退出),会有什么现象
- 如果服务端关闭这个connfd,客户端的connfd又有什么现象
但是这里先不分析,稍后再说
<2> 我们再来分析下服务端的程序
从上面可以看出,服务器关闭connfd的方式:
- 服务器进程别杀死
- 服务器进程读取出错
- 客户端关闭,read会返回0,从而退出str_echo,然后调用
close
那么这里也分为两种情况
- 服务器主动关闭
- 客户端主动关闭
总结: 由上面两个分析可以看出,只有客户端或者服务端进程退出,它们之间的通信才会停止
问: 此时我们主动关闭客户端会有什么现象呢?(此时只有一个客户端连接到了服务器上)
$ netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:54972 localhost:9877 ESTABLISHED
tcp 0 0 localhost:9877 localhost:54972 ESTABLISHED
$ pkill -9 vvv
$ netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:54972 localhost:9877 TIME_WAIT
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
6466 4134 pts/1 S ./AAA inet_csk_accept
由上面分析可知道:
对于服务端:
- 服务器进程的连接套接字马上关闭
- 服务器进程的监听套接字没有影响
- 服务器进程阻塞于等待下一个连接
inet_csk_accept
(accept()功能是从已经连接的请求队列中取出一个已完成握手的连接,但是这个时候已连接队列为空,所以阻塞等待。TCP三次握手是listen函数完成的)
对于客户端
- 客户端的监听套接字进入了
TIME_WAIT
状态 TIME_WAIT
是主动发起FIN请求的那一段进入的状态,由两个作用:- 确保主动方能够ACK被动方的所有FIN(当主动方的ACK丢失了,被动方会重发FIN,如果此时主动方关闭了,被动方将收到RST[没有进程在监听这个断开])
- 确保被动方的所有FIN都消逝了(当主动方的ACK丢失了,被动方会重发FIN,FIN可能会被路由器缓存,如果上一次FIN还没有消逝,又有同样端口同样地址同样序列号的主动方重新启动,这可能造成这个刚刚启动的进程被终止)
问:如果是服务器主动关闭,而客户端没有关闭呢?
$ netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:9877 localhost:55020 ESTABLISHED
tcp 0 0 localhost:55020 localhost:9877 ESTABLISHED
$ pkill -9 AAA
[1]+ 已杀死 ./AAA
$ netstat -a | grep 9877
tcp 0 0 localhost:9877 localhost:55020 FIN_WAIT2
tcp 1 0 localhost:55020 localhost:9877 CLOSE_WAIT
当我们kill服务器进程:
- 服务器会向客户端发送一个FIN,然后进入
FIN_WAIT1
状态 - 客户端(内核)收到这个FIN之后,会返回ACK FIN(当服务器收到ACK FIN之后,就会进入
FIN_WAIT2
),内核告知这个进程请关闭这个连接套接字,然后将 这个连接套接字置CLOSE_WAIT
。从下图可以看出客户端无法关闭连接套机字。也就是说客户端将一直处于CLOSE_WAIT
状态;服务器将一直处于FIN_WAIT2
,也就是阻塞在close
函数这里 - 最终导致这两个连接套接字都没有关闭(而每个进程能够开启的套接字的数量是有限的,我们应该避免这种情况的出现)
fget返回NULL的情况(跳出循环) - 如果成功,该函数返回相同的 sendline参数。
- 如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
- 如果发生错误,返回一个空指针。
如果此时我们在客户端中写入数据,现象是:
分析:
问: 当前状态服务器正常开启,而且有一个客户端连接上来了。此时如果有另外一个客户端请求连接服务器会发生什么样的情况呢?
netstat -a | grep 9877
tcp 1 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:55054 localhost:9877 ESTABLISHED
tcp 0 0 localhost:9877 localhost:55054 ESTABLISHED
tcp 0 0 localhost:9877 localhost:55052 ESTABLISHED
tcp 0 0 localhost:55052 localhost:9877 ESTABLISHED
从上面可以看出,可以有多个客户端连到当前服务器上
从这里也可以验证,TCP的三路握手是由listen
函数监听到请求,然后内核完成这个过程的。而accept的功能只是从已完成连接队列中取出队首元素而已。
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4134 3814 pts/1 Ss+ /bin/bash --rcfile /home/oc n_tty_read
8082 4134 pts/1 S ./AAA sk_wait_data
此时AAA进程阻塞在超时事件或者网络事件中(sk_wait_data
)
我们在第一个客户端输入数据:
我们在第二个客户端输入数据:
接下来我们分析一下为什么会出现这种现象把。
<1> 服务端
<2> 客户端
接下来我们来验证我们想的是不是正确!
客户端程序添加打印,然后重新编译并开启一个新的客户端
从下面我们可以看出,我们分析的是对的
关闭这个客户端,我们回到上面。
问:如果第一个客户端关闭了,第二个客户端先前写入的数据会回显吗?
答案是:会
当第一个客户端(sockfd
)关闭时:
- read阻塞返回一个0,然后就会关闭服务器的connfd
- accept会从已连接队列中取出第一个connfd,这个时候服务器发现这个connfd的缓冲区中有数据,就会读取然后写回去。
并发TCP
服务端
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buf[MAXLINE];
pid_t childpid;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = accept(listenfd, (SA *) NULL,NULL);
if( (childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
// fork为每个客户派生一个处理它们的子进程:子进程关闭监听套接字,父进程关闭已连接套接字。子进程接着调用str_echo处理客户
close(connfd);
}
close(listenfd);
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0){
write(sockfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0){
printf("str_echo: read error");
exit(0);
}
}
客户端
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2){
printf("usage: tcpcli <IPaddress>");
exit(0);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL) {
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli: server terminated prematurely");
exit(0);
}
fputs(recvline, stdout);
}
}
分析
服务器正常启动
对于服务端,初始时:被阻塞于accept,等待下一个连接
$ netstat -anp | grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 4842/./AAA
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4244 3899 pts/1 Ss /bin/bash --rcfile /home/oc do_wait
4842 4244 pts/1 S+ ./AAA inet_csk_accept
一个客户端连接到服务器
当第一个客户端调用socket和connect,后者引起TCP的三路握手过程。当三路握手完成之后,客户中的connect和服务器中的accept均返回,连接于是建立。
- 对于服务器,如下:
多进程
$ netstat -anp | grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 5287/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:55874 ESTABLISHED 5289/./AAA
tcp 0 0 127.0.0.1:55874 127.0.0.1:9877 ESTABLISHED 5288/./vvv
对比单进程
$ netstat -anp |grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 6466/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:54972 ESTABLISHED 6466/./AAA
tcp 0 0 127.0.0.1:54972 127.0.0.1:9877 ESTABLISHED 6512/./vvv
从上面的最后一列:当有一个客户端连接上来之后,单进程的监听套接字的ID和连接套接字的ID均为6466;多进程的监听套接字为5287,连接套接字为5289
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
5287 4244 pts/1 S+ ./AAA inet_csk_accept
5289 5287 pts/1 S+ ./AAA sk_wait_data
连接套接字5289 的父进程为监听套接字5287。STAT中的S表示睡眠。inet_csk_accept
表示阻塞在accept,sk_wait_data表示进程阻塞于套接字输入或者输出。
当连接建立之后,接下来发生的步骤如下:
- 客户调用str_cli函数,该函数阻塞与fget调用,等待终端输入
- 当服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo。该函数调用read_line,readline调用read,而read在等待客户输入(阻塞)
- 另一方面,服务器父进程再次调用accept并阻塞,等待下一个客户端连接
至此,我们有三个睡眠(也就是阻塞)进程:客户进程,服务器父进程、服务器子进程
客户端退出而服务器不退出
我们关闭客户端(CTRL+D)
$ netstat -anp | grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 5287/./AAA
tcp 0 0 127.0.0.1:55874 127.0.0.1:9877 TIME_WAIT
-
- 当我们键入EOF字符时,
fgets
返回一个空指针,于是std_cli
返回 - 当str_cli返回之后,mian调用
exit
终止。进程终止处理的部分工作是关闭所有打开的描述符,因此客户打开的套接字由内核关闭。这导致客户TCP发送一个FIN给服务器,服务器TCP则ACF FIN。至此,服务器处于CLOSE_WAIT
状态,客户套接字处于FIN_WAIT_2
状态 - 当服务器TCP收到FIN时,服务器子进程阻塞于
read_line
,于是read_line
返回0.这导致服务器子进程的main函数调用exit终止。 - 服务器子进程中打开的所有描述符随之关闭。由子进程来关闭已连接套接字会引发TCP连接终止的最后两个分节:一个服务器到客户的FIN和一个客户到服务器的ACK。至此,连接完全终止,客户套接字进入TIME_WAIT
- 进程终止处理的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号。但是我们没有在代码中处理该信号,该信号的默认行为是被忽。因为父进程没有处理,所以子进程进入僵尸状态,从下面就可以看出,STAT的状态是z(僵尸)
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
5287 4244 pts/1 S+ ./AAA inet_csk_accept
5289 5287 pts/1 Z+ [AAA] <defunct> do_exit
我们必须清理僵尸进程
处理僵尸进程
设置僵尸状态的目的是为了维护子进程的状态,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状态以及资源利用信息(CPU时间,内存使用量等等)
我们显然不愿意留存僵尸进程,它们占用内核中的空间,最终可能导致我们耗尽进程资源。 当我们fork出子进程时,可以掉wait和waitpid来等待子进程终止,防止其变成僵尸进程。
/*
* 作用: 如果调用wait的进程没有已经终止的子进程,不过还有一个或者多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止
* 参数:
* * __pid:指定想要等待的进程ID,值-2表示等待第一个终止的子进程
* * __stat_loc: 可以通过__stat_loc指针返回子进程终止状态
* * __options: 当为WNOHANG时,表示告知内核在没有已终止子进程时不要苏州
* 返回值:
* 错误返回0或者-1,成功返回已终止的进程ID
*/
__pid_t wait (__WAIT_STATUS __stat_loc);
__pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options);
当子进程终止时,会向父进程发送一个SIGCHLD,僵尸进程是因为父进程没有出终止的子进程发来的SIGCHLD状态,我们可以在父进程中设置一个信号处理函数,。我们可以在父进程中设置一个信号处理函数,捕获并处理这个函数(干的唯一事情就是等待子进程wait完成),为此服务端可以修改为:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func; // 设置信号处理函数
/*设置信号处理函数的信号掩码:POSIX允许我们指定这样一组信号,它们在
信号处理函数被调用时阻塞(注意目的并不是等待资源可用,而是防止它们在阻塞期间递交,直到解阻塞。)。
任何阻塞的都不能递交给进程。我们把sa_mask设置为null,表示在该信号处理函数运行期间,不阻塞额外的信号。
POSIX信号保证被捕获的信号在其信号处理函数运行期间总是阻塞的
*/
sigemptyset(&act.sa_mask);
//
act.sa_flags = 0;
if (signo == SIGALRM) { // 进程收到SIGALRM信号后。缺省的动作就是终止当前进程。
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
} else {
#ifdef SA_RESTART // 设置SA_RESTART之后:由相应信号中断的系统调用将由内核自动重启
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
/* end signal */
Sigfunc *Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR){
printf("signal error");
exit(0);
}
return(sigfunc);
}
/*
在SystemV和Unix98标准下,如果一个进程把SIGCHLD的处置设置为SIG_IGN,它的子进程就不会变为僵尸进程。不幸的是,但是仅仅适用于SystemV和UNIX98。处理僵尸进程的可移植方法是捕获SIGCHLD,并调用wait或者waitpid
*/
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) { //若在option中设置WNOHANG位,与那么该系统调用就是非阻塞的
printf("child %d terminated\n", pid);
}
return;
}
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buf[MAXLINE];
pid_t childpid;
void sig_chld(int);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld);
for ( ; ; ) {
connfd = accept(listenfd, (SA *) NULL,NULL);
if( (childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0){
write(sockfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0){
printf("str_echo: read error");
exit(0);
}
}
单个客户端的代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2){
printf("usage: tcpcli <IPaddress>");
exit(0);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL) {
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli: server terminated prematurely");
exit(0);
}
fputs(recvline, stdout);
}
}
我们来观察下单个客户端连上具有信号处理函数的并发服务器的现象:
- 先启动服务器,然后启动客户端,并确保回显正常
- 然后在客户端中EOF(ctrl +D) ,然后使用ps和netstat工具观察
分析:
- 当我们键入一个EOF字符来终止客户。客户TCP发送一个FIN给服务器,服务器响应一个ACK
- 收到客户ACK的FIN导致服务器TCP递送一个EOF给子进程阻塞的readline,从而子进程终止。
- 当SIGCHLD信号递交时,父进程阻塞于accept调用。sig_chld函数执行,其wait调用取到子进程的PID和终止状态。随后是printf调用,最后返回
效果:当没有僵尸进程
// 当客户端退出之后
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4244 3899 pts/1 Ss /bin/bash --rcfile /home/oc do_wait
7216 4244 pts/1 S+ ./AAA inet_csk_accept
问:如果有多个客户端连接到服务端会怎样?
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define SERV_PORT 9877
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd[5], i;
struct sockaddr_in servaddr;
if (argc != 2){
printf("usage: tcpcli <IPaddress>");
exit(0);
}
for (i = 0; i < 5; i++) {
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]); /* do it all */
exit(0);
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL) {
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli: server terminated prematurely");
exit(0);
}
fputs(recvline, stdout);
}
}
客户建立5个与服务器的连接,随后在调用str_cli函数时仅用第一个连接。建立多个连接的目的时从并发服务器上派生多个子进程
当客户终止时,所有打开的描述符由内核自动关闭(不调用close,仅调用exit)。而且所有5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个,它们反过来使服务器的5个子进程基本在同一时刻终止,这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进程
我们来观察下单个客户端连上具有信号处理函数的并发服务器的现象:
- 先启动服务器,然后启动客户端,并确保回显正常
$ netstat -anp | grep 9877
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN 8706/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:55926 ESTABLISHED 8730/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:55922 ESTABLISHED 8728/./AAA
tcp 0 0 127.0.0.1:55920 127.0.0.1:9877 ESTABLISHED 8726/./vvv
tcp 0 0 127.0.0.1:9877 127.0.0.1:55928 ESTABLISHED 8731/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:55924 ESTABLISHED 8729/./AAA
tcp 0 0 127.0.0.1:9877 127.0.0.1:55920 ESTABLISHED 8727/./AAA
tcp 0 0 127.0.0.1:55928 127.0.0.1:9877 ESTABLISHED 8726/./vvv
tcp 0 0 127.0.0.1:55924 127.0.0.1:9877 ESTABLISHED 8726/./vvv
tcp 0 0 127.0.0.1:55926 127.0.0.1:9877 ESTABLISHED 8726/./vvv
tcp 0 0 127.0.0.1:55922 127.0.0.1:9877 ESTABLISHED 8726/./vvv
从上面可以看出: 客户端vvvv有5个连接套接字,ID均为8726。服务器AAA有1个监听套接字,5个连接套接字,它们的ID各不相同
$ ps -t pts/1 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4244 3899 pts/1 Ss /bin/bash --rcfile /home/oc do_wait
8706 4244 pts/1 S+ ./AAA inet_csk_accept
8727 8706 pts/1 S+ ./AAA sk_wait_data
8728 8706 pts/1 S+ ./AAA sk_wait_data
8729 8706 pts/1 S+ ./AAA sk_wait_data
8730 8706 pts/1 S+ ./AAA sk_wait_data
8731 8706 pts/1 S+ ./AAA sk_wait_data
从上面可以看出,服务器AAA均在阻塞睡眠中(S)。其中监听套接字阻塞等待accept,5个连接套接字阻塞于套接字read(客户端)。并且5个连接套接字的均由监听套接字fork而来
$ ps -t pts/2 -o pid,ppid,tty,stat,args,wchan
4309 3899 pts/2 Ss /bin/bash --rcfile /home/oc do_wait
8726 4309 pts/2 S+ ./vvv 127.0.0.1 n_tty_read
从上面可以看出,仅有一个客户端套接字在阻塞等待标准输入
- 然后在客户端中EOF(ctrl +D) ,然后我们观察现象
没有僵尸进程
客户端正常而服务器进程终止
当服务器进程崩溃时,客户端会发生什么情况?
接下来,我们模拟这个现象:
-
在同一个主机上启动服务器和客户,并在客户上键入一行文本,以验证一切正常,正常情况下该行文本由服务器子进程回射给客户
-
找到服务器子进程的进程ID,并执行kill杀死。作为进程终止处理的部分工作,子进程中所有打开着的描述符都被关闭。这就导致向客户发送一个FIN,而客户TCP则响应一个ACK FIN。
-
SIGCHLD信号被发送给服务器父进程,并得到正确处理
-
-
客户上没有发生任何特殊之事情,客户TCP接收来自服务器TCP的FIN并响应一个ACK,然而问题是客户进程阻塞在fget上,等待从终端接收一行文本
-
此时,我们ps和netstat,观察套接字状态
从上面我们可以看到,TCP连接终止序列的前半部分已经完成。 -
我们在客户端上在输入一行文本,报错退出:
因为服务器的那个sockfd已经关闭,我们不可能从这个sockfd中read。具体分析如下:
总结:这个例子的问题在于:客户正阻塞在fget上。客户实际上在应对两个描述符:套接字和用户输入,它不能单纯阻塞在这两个源上的某个特定源的输入上,而是应该阻塞在其中的任何一个源的输入上。这也是为什么要引入select
和poll
的目的之一。
ps:
- 调用read之后,收到一个RST,返回一个EOF,表示服务器提前关闭
- 收到一个RTS之后,调用read,返回一个ECONNRESET(
connection reset by peer
),表示复位连接错误
SIGPIPE【怎么模拟??】
如果客户端不理会read返回的EOF,反而写入更多的数据到服务器上,会发生什么呢?
问:怎么模拟
对一个已经关闭的通道第一次写引发RST(服务器提前关闭错误),第二次写引发SIGPIPE信号。
问:怎么分析
服务器主机崩溃【怎么模拟??】
问: 怎么模拟
1、在不同的主机上运行客户和服务器:先启动服务器,再启动客户端,并确保客户端回射正
2、从网络上断开主机
3、在客户端输入文本(此时也模拟了客户端发送数据时主机不可达)
问:怎么分析
。。。。。。
如果不使用SO_KEEPALIVE
套接字选项,那么如果在服务器主机崩溃时(后)客户不主动给服务器发送数据,那么客户将不知道服务器主机已经崩溃。
如果我们想要不主动向服务器发送数据就检测出服务器主机的崩溃,就需要SO_KEEPALIVE
选项或者使用心跳检测
服务器主机崩溃后重启【怎么模拟??】
问:适用于什么场景
如果需要在发送数据前重新启动已经崩溃的主机,不想要客户知道服务器的关机
问: 怎么模拟
问: 怎么分析
- 启动服务器和客户,并在客户键入一行文本以确认连接已经建立
- 服务器主机崩溃并重启
- 在客户上键入一行文本,它将作为一个TCP数据分节发送给服务器主机
- 当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST
- 当客户TCP收到该RST时,客户正阻塞于read调用,导致该调用返回
ECONNRESET
错误
服务器主机关机
Unix系统关机时,init进程通常先给所有进程发送SIGTERM
信号(可被捕获),等待一段固定的时候(5-20s),然后给所有仍在运行的进程发送SIGKILL信号(不可被捕获)。
补充
ps
当进程处于睡眠状态时,WCHAN指出相应的条件:
- 在进程阻塞于套接字输入或者输出的时候,输出
tcp_data_wait
;- 当进程阻塞于套机字输入或者输出时,输出
tcp_data_wait
- 在进程阻塞于终端IO时,输出
read_chan
inet_csk_accept
请求队列中没有等待握手的连接,又因为listen套接字在默认是阻塞的,所以这个套接字就阻塞到了accept状态
- ps a 显示现行终端机下的所有程序,包括其他用户的程序。
- ps -t<终端机编号> 指定终端机编号,并列出属于该终端机的程序的状况。
使用ps命令查看进程的当前状态,其中STAT列的含义如下:
- D 不可中断的休眠。通常是IO。
- R 运行。正在运行或者在运行队列中等待。
- (+) 位于前台进程组。
- S 休眠。在等待某个事件,信号。
- T 停止。进程接收到信息SIGSTOP,SIGSTP,SIGTIN,SIGTOU信号。
- X 死掉的进程,不应该出现。
- Z 僵死进程。
信号
信号就是告知某个进程发生了某个事件的通知,有时也成为软件中断。信号是异步发生的,也就是说进程预先不知道信号发生的准确时机
信号可以:
- 由一个进程发给另一个进程(或者自身)
- 由内核发给某个进程
上面的SIGCHLD
信号就是由内核在任何一个进程终止时发给它的父进程的一个信号。
每个信号都有一个与之关联的处置(行为),我们通过调用sigaction
函数来设定一个信号的处理,它有三种选择:
- 提供一个信号处理函数,只要有特定信号
发生它就被调用(捕获信号)。有两个信号不能被捕获,它是SIGKULL和SIGSTOP。信号处理函数的原型如下:
void handler(int sigo);
- 把某个信号的处置设定为
SIG_ING
来忽略它。SIGKULL和SIGSTOP不能被忽略 - 把某个信号的处置设定为
SIG_DFL
来启用它的默认处理。默认处理一般是收到信号后终止进程,其中某些信某些信号还在当前工作目录产生一个进程的core image。
关于信号处理函数:
- 在一个信号处理函数运行期间,正被递交的信号是阻塞的。而且,安装处理函数时在传递给
sigaction
函数的>sa_mask
信号集中指定的任何额外信号也被阻塞。我们可以使用sigemptyset
将sa_mask
置为空集,表示除了被捕获的信号外,没有额外信号被阻塞- 如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常值递交一次,也就是说Unix信号默认是不排队的。
- 利用
sigprocmask
函数选择性的阻塞或者解阻塞一组信号是可能的。这使得我们可以做到在一段临界区代码执行期间,防止捕获某些信号,以此保护这段代码