socket编程---TCP
1.socket函数
int socket(int protofamily, int type, int protocol);//返回sockfd,描述符
protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:
并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口
实现
当服务器程序调用socket系统调用之后,内核会创建一个struct socket和一个struct sock结构,两者可以通过指针成员变量相互访问对方。内核直接操作的是struct sock结构。struct socket的存在是为了适应linux的虚拟文件系统,把socket也当作一个文件系统,通过指定superblock中不同的操作函数实现完成相应的功能。在linux内核中存在不同的sock类型,与TCP相关的有struct sock、 struct inet_connection_sock,、struct tcp_sock等。这些结构的实现非常灵活,可以相互进行类型转换。这个机制的实现是结构体的一层层包含关系:struct tcp_sock的第一个成员变量是struct inet_connection_sock,struct inet_connection_sock的第一个成员变量是struct sock。
通过这种包含关系,可以将不同的sock类型通过指针进行相互转换。比如:
struct tcp_sock tcp_sk; struct sock *sk = (struct sock *)&tcp_sk;
为了避免从小的结构体转换到大的结构体造成内存越界,对于TCP协议,内核在初始化一个stuct sock时给它分配的空间大小是一个struct tcp_sock的大小。这样sock类型的相互转换便可以灵活的进行。另外,在内核创建完sock和socket之后,还需要绑定到对应的文件描述符以便应用层能够访问。一个task_struct中有一个文件描述符数组,存储所有该进程打开的文件,因为socket也可以看做是文件,也存储在这个数组中。文件描述符就是该socket在该数组中的下标,具体的实现请参照虚拟文件系统。
2.bind函数
bind()函数把一个地址族中的特定地址(本地协议地址)赋给socket(即地址的绑定)。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 限定了只接受地址为addr的客户信息,若服务器没有绑定ip地址,则内核就把客户端发送的SYN目的地址作为服务器的源IP地址,一般我们捆绑统配地址:INADDR_ANY,告诉系统,若系统是多宿主机,我们将接受目的地址为任何本地接口的连接。
addrlen:对应的是地址的长度。
错误信息:
- EACCES:地址受到保护,用户非超级用户。
- EADDRINUSE:指定的地址已经在使用。
- EBADF:sockfd参数为非法的文件描述符。
- EINVAL:socket已经和地址绑定。
- ENOTSOCK:参数sockfd为文件描述符。
注意:
- 如果TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时内核就选择一个临时端口,这对客户来说是正常的,服务器应该调用众所周知的端口
- 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,务必将其转化为网络字节序再赋给socket
实现
该调用通过传递进来的文件描述符找到对应的socket结构,然后通过socket访问sock结构。操作sock进行地址的绑定。如果指定了端口检查端口的可用性并绑定,否则随机分配一个端口进行绑定。但是怎样获知当前系统的端口绑定状态呢?通过一个全局变量inet_hashinfo进行,每次成功绑定一个端口会都将该sock加入到inet_hashinfo的绑定散列表中。加入之后bind的系统调用已基本完成了。
3.listen函数
int listen(int sockfd, int backlog);
- 第一个参数即为要监听的socket描述字
- 第二个参数为相应socket可以排队的最大连接个数
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求,把CLOSED状态转换为LISTEN状态。
未完成队列:
每个这样的SYN分节对应其中一项,客服发送至服务器,服务器等待完成TCP的三次握手,这些套接字处于SYN_RCVD状态。
已完成队列:
每个已完成的TCP完成三路握手的客户对应其中一项,这些套接字处于ESTABLISHED状态。
注意:
- 每在未完成队列中创建一项时,来自监听套接字的参数就立即复制到建立连接中,链接创建自动完成。
- 来自客户的SYN到达时,TCP在未完成对队列中创建一项,然后相应三路握手的第二个分节:服务器SYN相应,捎带对客户的SYN分节的ACK,这一项一直保留在未完成队列中,直到三路握手的第三个分节客户对服务器的SYN的ACK到达或该项超时为止。
- 已完成队列的对头返回给进程,如果进程为空,队列被投入睡眠,直到TCP在该队列中放一项为止;若当客户的一个SYN到达时,这些队列是满的,TCP就忽略该分节,也就是不发送RST,因为这些情况是暂时的,期望不就就能在这些队列中找到一个可用的空间,若服务器响应RST,客户端connect调用就会返回一个错误。
- backlog不能为0
实现
和listen相关的大部分信息存储在inet_connection_sock结构中。同样的内核通过文件描述符找到对应的sock,然后将其转换为inet_connection_sock结构。在inet_connection_sock结构体中含有一个类型为request_sock_queue的icsk_accept_queue变量,存储一些希望建立连接的sock相关的信息。结构为:
struct request_sock_queue { struct request_sock *rskq_accept_head; struct request_sock *rskq_accept_tail; rwlock_t syn_wait_lock; u8 rskq_defer_accept; struct listen_sock *listen_opt; };
listen_opt用了存储当前正在请求建立连接的sock,称作半连接状态,用request_sock表示。request_sock有个成员变量指针指向对应的strut sock。rskq_accept_head和rskq_accept_tail分别指向已经建立完连接的request_sock,称作全连接状态,这些sock都是完成了三次握手等待程序调用accept接受。程序调用listen之后会为icsk_accept_queue分配内存,并且将当前的监听sock放到全局变量inet_hashinfo中的监听散列表中。当内核收到一个带有skb之后会通过tcp_v4_rcv函数进行处理。因为只有skb,还需找到对应的sock。该过程通过 __inet_lookup_skb进行实现。该函数主要调用__inet_lookup,其中:
- 首先看看这个包是不是一个已经建立好连接的sock中的包,通过__inet_lookup_established函数进行操作(一个连接通过源IP,目的IP,源PORT和目的PORT标识)。
- 失败的话可能是一个新的SYN数据包,此时还没有建立连接所以没有对应的sock,和该sock相关的只可能是监听sock了。
所以通过__inet_lookup_listener函数找到在本地的监听对应端口的sock。无论哪种情况,找到sock之后便会将sock和skb一同传入tcp_v4_do_rcv函数作统一处理
if (sk->sk_state == TCP_ESTABLISHED) { sock_rps_save_rxhash(sk, skb->rxhash); TCP_CHECK_TIMER(sk); if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } TCP_CHECK_TIMER(sk); return 0; } if (sk->sk_state == TCP_LISTEN) { struct sock *nsk = tcp_v4_hnd_req(sk, skb); if (!nsk) goto discard; if (nsk != sk) { if (tcp_child_process(sk, nsk, skb)) { rsk = nsk; goto reset; } return 0; } } if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; }
- 如果是一个已建立连接的sock,调用tcp_rcv_established函数进行相应的处理。
- 如果是一个正在监听的sock,需要新建一个sock来保存这个半连接请求,该操作通过tcp_v4_hnd_req实现。
这里我们只关注tcp的建立过程,所以只分析tcp_v4_hnd_req和tcp_child_process函数:
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb) { struct tcphdr *th = tcp_hdr(skb); const struct iphdr *iph = ip_hdr(skb); struct sock *nsk; struct request_sock **prev; struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr); if (req) return tcp_check_req(sk, skb, req, prev); nsk = inet_lookup_established(sock_net(sk), &tcp_hashinfo, iph->saddr, th->source, iph->daddr, th->dest, inet_iif(skb)); if (nsk) { if (nsk->sk_state != TCP_TIME_WAIT) { bh_lock_sock(nsk); return nsk; } inet_twsk_put(inet_twsk(nsk)); return NULL; } return sk; }
- 首先调用inet_csk_search_req查找在半连接队列中是否已经存在对应的request_sock。有的话说明这个请求连接已经存在,调用tcp_check_req处理第三次握手的情况,当sock的状态从SYN_RCV变迁到ESTABLISHED状态时,连接建立完成。需要将该request_sock从request_sock_queue队列中的listen_opt半连接队列取出,放入全连接队列等待进程调用accept取走,同时是request_sock指向一个新建的sock并返回。
- 没有的话调用inet_lookup_established从已经建立连接sock中查找,如果找到的话说明这是一条已经建立的连接,当该sock不处于timewait将sock返回状态时将sock返回,否则返回NULL。
- 当上述两种情况都失败了,表示这是一个新的为创建的连接,直接返回sk。这样通过tcp_v4_hnd_req函数就能够找到或创建和这个skb相关的sock。
A.如果返回的sock和处于Listen状态的sock不同,表示返回的是一个新的sock,第三次握手已经完成了。调用tcp_child_process处理。该函数的逻辑是让这个新的tcp_sock开始处理TCP段,同时唤醒应用层调用accept阻塞的程序,告知它有新的请求建立完成,可以从全连接队列中取出了。
B.如果返回的sock没有变化,表示是一个新的请求,调用tcp_rcv_state_process函数处理第一次连接的情况。该函数的逻辑较为复杂,简单的可以概括为新建一个request_sock并插入半连接队列,设置该request_sock的sock为SYN_RCV状态。然后构建SYN+ACK发送给客户端,完成TCP三次握手连接的第二步。
4.connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。
对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。
注意:
- 若TCP客户没75s有收到SYN分节的响应(时隔6s发一个),返回ETIMEDOUT,
- 对客户的SUN分节的相应是RST,表明服务器主机在我们指定的端口无进程等待,或服务器没有运行,硬错误,返回ECONNREFUSED
- 若客户发生ICMP(目的地不可到达错误),软错误,客户内核保存该消息,并按照第一种情况再次发送,若75s后还没相应,咋把内核保存的EHOSTUNREACH或ENETUNREACH返回给进程,以下两种情况也可能一:按照本地系统的转发没有到达远程系统的路径。二:connect不等待就返回
实现
connect系统调用根据文件描述符找到socket和sock,如果当前socket的状态时SS_UNCONNECTED的情况下才正常处理连接请求。首先调用tcp协议簇的connect函数(即tcp_v4_connect)发送SYN,然后将socket状态置为SS_CONNECTING,将进程阻塞等待连接的完成。剩下的两次握手由协议栈自动完成。
tcp_v4_connect函数:该函数首先进行一些合法性验证,随后调用ip_route_connect函数查找路由信息,将当前sock置为SYN_SENT状态,然后调用inet_hash_connect函数绑定本地的端口,和服务器端绑定端口的过程类似,但是会额外的将sock添加inet_hashinfo中的ehash散列表中(添加到这的原因是因为希望以后收到的SYN+ACK时能够找到对应的sock,虽然当前并没有真正意义上的建立连接)。到最后调用tcp_connect构建SYN数据包发送。
tcp_connect:该函数逻辑比较简单,构造SYN数据段并设置相应的字段,将该段添加到发送队列上调用tcp_transmit_skb发送skb,最后重设重传定时器以便重传SYN数据段。
当客户端收到SYN+ACK之后,首先会通过tcp_v4_rcv从已建立连接散列表中找到对应的sock,然后调用tcp_v4_do_rcv函数进行处理,在该函数中主要的执行过程是调用tcp_rcv_state_process。又回到了tcp_rcv_state_process函数,它处理sock不处于ESTABLISHED和LISTEN的所有情况。当发现是一个SYN+ACK段并且当前sock处于SYN_SENT状态时,表示连接建立马上要完成了。首先初始化TCP连接中一些需要的信息,如窗口大小,MSS,保活定时器等信息。然后给该sock的上层应用发送信号告知它连接建立已经完成,最后通过tcp_send_ack发送ACK完成最后一次握手。
5.accept函数
在三次握手之后
服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
addr
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
len
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。
注意:
- accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字
- 连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号
需要区分两种套接字
- 监听套接字:监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
- 连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
实现
该调用创建新的struct socket表示新的连接,搜寻全连接队列,如果队列为空,将程序自身挂起等待连接请求的完成。否则从队列中取出头部request_sock,并设置新的struct socket和request_sock中的struct sock的对应关系。这样一个连接至此就建立完成了。客户端可以通过新返回的struct socket进行通信,同时旧的struct socket继续在监听。
服务器代码
- fork子进程时(一个服务器同一时刻只能处理一个客户,提高并发度),必须捕获SIGCHLD信号
- 当信号捕获时,必须处理被中断系统调用
- SIGCHLD(子进程结束后向父进程发送的信号)的信号处理函数必须正确编写,用waitpid函数以免留下僵尸进程
#include "unp.h" #include "my_err.h" /* * 若使用wait,会导致使用wait的进程(父进程)阻塞,直到子进程结束或者收到一 *信号为止;如果在同一台主机上运行,启动多个客户信号处理函数只会执行一次,在 *不同主机上运行,信号处理函数执行两次,一次是产生信号,一次是其他信号处理 *函数在执行时发生,也不能如下在循环中调用wait:没有办法防止wait正在运行的 *子进程尚有未终止的阻塞。 * 用waitpid,指定WNOHANG选项,提供一个非阻塞的wait版本,pid=-1等待任 *何一个子进程退出,与wait作用一样,所以用waitpid。1.在信号处理函数中,如果 *有子进程终止,通过while一次性回收2.非阻塞模式:保证回收最后一个中止的子进 *程后,没有子进程退出时,waitpid返回出错,主进程从信号处理函数中跳出而不是 *阻塞在信号处理函数中 */ void header(int signo) { pid_t pid; int stat; while((pid=waitpid(-1,&stat,WNOHANG))>0) printf("child %d terminated\n",pid); return; } void str_echo(int sockfd) { ssize_t n; char buffer[MAXLINE]; again: while((n=read(sockfd,buffer,MAXLINE))>0) write(sockfd,buffer,n); //处理被中断的慢系统调用 if(n<0&&errno==EINTR) goto again; else if(n<0) err_sys("str_echo:read error"); } int main() { int listenfd=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in cliaddr,servaddr; 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,(struct sockaddr *)&servaddr,sizeof(servaddr)); listen(listenfd,LISTENQ); /* *客户套接字发送FIN给服务器,服务器收到回应ACK以确认,这就是TCP的终止链 *的前半部分 *当服务器TCP收到FIN时,该连接的读半部关闭,read返回0,str_echo返回,至 *子进程服务器exit终止返回至main函数,子进程服务器打开的所有描述符全部 *关闭,由子进程关闭套接字会发tcp终止连接的最后连个分节,一个服务器到 *客户的FIN和客户到服务器的ACK,至此完全终止链接,客户套接字进入 *TIME_WAIT状态。由于任何子进程终止时会给父进程发放送一个SIGCHLD信号,该 *信号的默认处理是忽略,父进程不处理此信号,子进程会进入僵尸状态,所以父 *进程要捕获该信号。 */ signal(SIGCHLD,header); while(1) { socklen_t clilen=sizeof(cliaddr); /* * 当一直没有客户连接到服务器,accept函数会阻塞,当在accept中阻塞时 * 收到某个信号且从信号处理程序中返回,这个系统调用会被中断,调用返 * 错误,设置errno为EINTR,对于accept系统调用要进行人为重启,但是co * nnect不能重启,否则会返回错误,处理方法是:用select等待连接而完成 *递交SIGCHLD信号时,父进程阻塞于accept调用 */ int connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen); if(connfd<0) if(errno==EINTR) continue; else err_sys("accept error"); pid_t cpid; //fork之后listenfd和connfd的引用计数都为2 if((cpid=fork())==0) { close(listenfd);//关闭监听套接字,时listenfd计数一直为一 str_echo(connfd); close(connfd); exit(0); } close(connfd);//新的客户由子进程提供服务,父进程可以关掉已连接套接字 } exit(0); }
客户代码
阻塞的客户端
#include "unp.h" #include "my_err.h" void str_cli(FILE *fp,int sockfd) { char buffer[MAXLINE]; int stdineof=0; fd_set rest; FD_ZERO(&rest); int n; while(1) { if(stdineof==0) FD_SET(fileno(fp),&rest); FD_SET(sockfd,&rest);//把socked描述符加入rest集合 int maxfd=max(fileno(fp),sockfd)+1; /* *客户端等待可读:标准输入可读或是套接字可读,select返回后会把以前 *加入的但并无时间发生的fd清空,所以每次select开始之前要把fd逐个加入 */ select(maxfd,&rest,NULL,NULL,NULL); /*等待套接字可读 * 1.对端tcp发送数据,该套接字变为可读,read并返回个大于0的数 * 2.对端tcp发送FIN(对端进程终止),该套接字变为可读,read返回0(EOF) * 3.对端tcp发送RST(对端主机)崩溃并重启,该套接字变为可读,read返回 * -1,errno中含有确切的错误代码 */ /*补充下服务器端的套接字可读或可写 * 1.accept成功之后便是可读 * 2.当客户端发送recv函数,服务器端便知道客户端可写, */ if(FD_ISSET(sockfd,&rest)) { if((n=read(sockfd,buffer,MAXLINE))==0) if(stdineof==1) return; else err_quit("str_cli: server terinated prematurely"); write(fileno(stdout),buffer,n); } //等待stdin可读,有数据就可读 if(FD_ISSET(fileno(fp),&rest)) { //客户端输入完成 if((n=read(fileno(fp),buffer,MAXLINE))==0) { stdineof=1; /*SHUT_WR * send FIN,留在当前缓冲区中的数据被发送,后跟TCP的终止序列 * 不论socket的引用是否为0,都关闭;在标准输入的方式下,输入 * 的eof并不以为socket同时也完成了读,有可能请求在区服务器的 * 路上,或者答应可能返回客户的路上,所以需要一种关闭tcp一般 * 的方法,给服务器发送FIN,告诉服务器我们已经完成了数据输入 * 但socket仍打开保持读。 */ shutdown(sockfd,SHUT_WR); //客户端完成了数据发送,要清除stdin文件描符,防止再次发送数 //据 FD_CLR(fileno(fp),&rest); continue; } write(sockfd,buffer,n); } } return ; } int main(int argc,char **argv) { //命令行获取服务器ip if(argc!=2) err_quit("please input tcplicent <IP-address"); int sockfd=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in servaddr; bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(SERV_PORT); inet_pton(AF_INET,argv[1],&servaddr.sin_addr);//把assic转换为二进制 connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); str_cli(stdin,sockfd); exit(0); }
connect出错
- 若TCP客户端没有收到syn分节的响应,则返回ETIMEOUT错误;调用connect函数时,内核发送一个syn,若无响应则等待6s后再发送一个,若仍然无响应则等待24s后在发送一个,若总共等待75s后仍未收到响应则返回本错误;
- 若对客户的syn响应是rst,则表明该服务器在我们指定的端口上没有进程在等待与之连接,这是一种硬错误,客户一收到rst马上返回ECONNREFUSED错误(产生RST三个条件:1.目的地为某个端口的SYN到达而端口上没有正在监听的服务器2.TCP想取消一个已有的连接3.TCP收到一个根本不存在的连接上的分节);
- 若客户发送的syn在中间的某个路由器上引发了目的不可达icmp错误,则认为是一种软错误。客户主机内核保存该消息,并按照第一种情况的时间间隔继续发送syn,咋某个规定时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或者ENETUNREACH错误返回给进程;
accept返回前连接中止
在比较忙的服务器中,在建立三次握手之后,调用accept之前,可能出现客户端断开连接的情况;如,三次握手之后,客户端发送rst,然后服务器调用accept。posix指出这种情况errno设置为CONNABORTED;注意Berkeley实现中,没有返回这个错误,而是EPROTO,同时完成三次握手的连接会从已完成队列中移除;在这种情况下,如果我们用select监听到有新的连接完成,但之后又被从完成队列中删除,此时如果调用阻塞accept就会产生阻塞;
解决办法:
- 使用select监听套接字是否有完成连接的时候,总是把这个监听套接字设置为非阻塞;
- 在后续的accept调用中忽略以下错误,EWOULDBLOCK(Berkeley实现,客户中止连接), ECONNABORTED(posix实现,客户中止连接), EPROTO(serv4实现,客户中止连接)和EINTR(如果有信号被捕获);
服务器进程终止(崩溃)
在客户端和服务器端建立连接之后,使用kill命令杀死服务器进程,进程终止会关闭所有打开的描述符,这导致了其向客户端发送了一个FIN,而客户端则响应了一个ack,这就完成了tcp连接终止的前半部分,只代表服务器不在发送数据了;但是客户端并不知道服务器端已经终止了,当客户端向服务器写数据的时候,由于服务器进程终止,所以响应了rst,如果我们使用select等方式,能够立即知道当前连接状态;如下:
- 如果对端tcp发送数据,那么套接字可读,并且read返回一个大于0的值(读入字节数);
- 如果对端tcp发送了fin(对端进程终止),那么该套接字变为可读,并且read返回0(EOF);
- 如果对端tcp发送rst(对端主机崩溃并重启),那么该套接字变为可读,并且read返回-1,errno中含有确切错误码;
sigpipe信号
当服务器关闭连接时,客户端收到FIN(read==0),但是FIN的接受并没有告知服务器已经终止连接,只是告诉了服务器不再向客户发送数据,若此时服务器又接收到来自客户端数据,因为先前打开的套接字的那个进程已被终止,所以此时回会相应一个RST。
当一个进程向某个收到rst的套接字执行写操作的时候,内核向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止;不论进程是捕捉了该信号并从信号处理函数中返回,还是简单忽略该信号,写操作都讲返回EPIPE错误;
服务器主机崩溃
建立连接之后,服务器主机崩溃,此时如果客户端发送数据,会发现客户端会在一定时间内持续重传,视图从服务器端收到数据的ack,当重传时间超过指定时间后,服务器仍然没有响应,那么返回的是ETIMEDOUT;
服务器主机不可达
建立连接之后,服务器主机未崩溃,但是由于中间路由器故障灯,判定主机或网络不可达,此时如果客户端发送数据,会发现客户端会在一定时间内持续重传,视图从服务器端收到数据的ack,当重传时间超过指定时间后,服务器仍然没有响应,那么返回的是EHOSTUNREACH或ENETUNREACH;
服务器主机崩溃后重启
当服务器主机崩溃重启后,之前所有的tcp连接丢失,此时服务器若收到来自客户端的数据,会响应一个rst;客户端调用read将返回一个ECONNRESET错误;
服务器主机关机
系统关机时,init进程给所有进程发送SIGTERM信号,等待固定的时间,然后给所有仍在运行的进程发送SIGKILL信号,我们的进程会被SIGTERM或者SIGKILL信号终止,所以与前面服务器进程终止相同,进程关闭所有描述符,并发送fin,完成服务器端的半关闭
以下code含以上问题及解决办法
客户
/************************************************************************* > File Name: client.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年03月04日 星期一 08时51分56秒 ************************************************************************/ #include <iostream> #include <sys/socket.h> #include <cstring> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <fcntl.h> #include <sys/select.h> #include <sys/time.h> using namespace std; void cli_echo(int sockfd) { //设置等待时间等待connect连接成功 struct timeval tval; tval.tv_sec=6; tval.tv_usec=0; fd_set set,wset; FD_ZERO(&set); FD_ZERO(&wset); string s; while(1) { FD_SET(fileno(stdin),&set); FD_SET(sockfd,&set); FD_SET(sockfd,&wset); //客户端对应两个输入,套接字和用户输入,他不能单纯的阻塞在某个中断 //上,应该阻塞在任何一个中断上 int maxfd=max(fileno(stdin),sockfd)+1; int res=select(maxfd,&set,&wset,NULL,&tval); if(res<=0) { cerr<<"connect time out"<<endl; close(sockfd); exit(0); } /* * socket描述符只可写,连接成功 * 若即可读又可写,分为两种情况: * 第一种:出错,因为可能是connect连接成功后远程主机断开连接close(socket) * 第二种:连接成功,socket读缓冲区得到了远程主机发送的数据,根据 * connect连接成功后errno的返回值来判定,或通过getsockopt函数返回值 * 来判断,但linux下getsockopt始终返回0,错误的情况下应返回-1 */ int n=0; if(FD_ISSET(fileno(stdin),&set)) { if((n=read(fileno(stdin),(void*)s.c_str(),1024))==0) { shutdown(sockfd,SHUT_WR); continue; } //只可写肯定返回成功 if(FD_ISSET(sockfd,&wset)&&!FD_ISSET(sockfd,&set)) { //3.对已经收到RST的套接字进行写操作,内核向进程发送SIGPIPE //第一次write引发RST,第二次产生SIGPIPE,如何在第一次写操作 //捕获SIGPIPE,做不到 write(sockfd,(void *)s.c_str(),1); sleep(1); write(sockfd,(void *)(s.c_str()+1),n-1); } } else if(FD_ISSET(sockfd,&set)&&FD_ISSET(sockfd,&wset)) { int err; socklen_t len=sizeof(err); //不通过getsockopt返回值判断,通过返回的参数判断 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&err,&len); if(err) { cerr<<err<<" :"<<strerror(err)<<endl; exit(0); } if((n=read(sockfd,(void *)s.c_str(),1024))>0) write(fileno(stdout),(void *)s.c_str(),n); if(n==0)//2.服务器端进程关闭,客户会收到服务器的一个RST { cerr<<strerror(errno)<<endl; exit(0); } } } return ; } int main() { int sockfd=socket(AF_INET,SOCK_STREAM,0); //禁用nagle算法 const char opt=1; setsockopt(sockfd,IPPROTO_TCP,TCP_NODELAY,&opt,sizeof(opt)); sockaddr_in servadddr; memset(&servadddr,sizeof(servadddr),0); servadddr.sin_family=AF_INET; servadddr.sin_port=htons(9877); inet_pton(AF_INET,"127.0.0.1",&servadddr.sin_addr); int res=connect(sockfd,(struct sockaddr *)&servadddr,sizeof(servadddr)); //1.connect返回立即发送RST struct linger l; l.l_onoff=1; l.l_linger=0; setsockopt(sockfd,SOL_SOCKET,SO_LINGER,&l,sizeof(l)); fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFL,0)|O_NONBLOCK); //res==0连接成功 //==-1开始三次握手但未完成 if(res==-1) { if(errno!=EINPROGRESS)//表示正在试图连接,不能表示连接失败 { //oeration now progress:套接字为非阻塞套接字,且原来的连接未完成 cout<<strerror(errno)<<endl; exit(0); } /** * 也可以在此处处理select连接 */ } cli_echo(sockfd); close(sockfd); return 0; }
服务器
/************************************************************************* > File Name: server.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年03月04日 星期一 09时35分26秒 ************************************************************************/ #include <iostream> #include <cstring> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <signal.h> #include <sys/wait.h> using namespace std; void header(int num) { pid_t pid; int stat; while((pid=waitpid(-1,&stat,WNOHANG))>0) cout<<"child:"<<pid<<"terminated"<<endl; return ; } void str_echo(int fd) { ssize_t n=0; char buf[1024]; again:while((n=read(fd,buf,1024))>0) write(fd,buf,n); //处理中断系统调用错误 if(n<0&&errno==EINTR) goto again; else if(n<0&&errno==ECONNRESET)//1. { cerr<<"reset by perr"<<endl; exit(0); } else cerr<<"read error"<<endl; } int main() { int sockfd=socket(AF_INET,SOCK_STREAM,0); int keepidle=1; setsockopt(sockfd,SOL_SOCKET,SO_KEEPALIVE,(void *)&keepidle,sizeof(keepidle)); sockaddr_in cliaddr,servaddr; memset(&servaddr,sizeof(servaddr),0); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(9877); servaddr.sin_addr.s_addr=htonl(0);//INADDR_ANY bind(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); listen(sockfd,1024); signal(SIGCHLD,header); while(1) { socklen_t len=sizeof(cliaddr); //1.模拟较忙的服务器 //sleep(10);//完成三路握手后客户发送RST(复位) conn:int connfd=accept(sockfd,(sockaddr *)&cliaddr,&len); //处理被中断的系统调用,因为在阻塞于某个中断时,这时候进来一个 //信号,执行信号处理函数返回后系统调用会返回EINTR if(connfd<0) { if(errno==EINTR) goto conn; else if(errno==ECONNABORTED) { cerr<<"accept:connect reset by peer"<<endl; exit(0); } } else { pid_t pid; if((pid=fork())==0) { close(sockfd); char des[100]; //cout<<getpid()<<endl; inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,des,sizeof(des)); cout<<"accept success,cliaddr is:"<<des<<endl; str_echo(connfd); close(connfd); exit(0); } close(connfd); } } return 0; }
在这些基础的socket API中,accept,connect,send,recv都可能是阻塞的,但是可以把他们编程非阻塞;
- 对于accept,send,recv来说,设置为非阻塞时errno返回值为-1且通常设置为EAGAIN(再来一次)或EWOULDBLOCk(期望阻塞)
- 对于connect,errno被置为EINPROGRESS(在处理中)