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:对应的是地址的长度。

错误信息:

  1. EACCES:地址受到保护,用户非超级用户。
  2. EADDRINUSE:指定的地址已经在使用。
  3. EBADF:sockfd参数为非法的文件描述符。
  4. EINVAL:socket已经和地址绑定。
  5. ENOTSOCK:参数sockfd为文件描述符。

注意:

  1. 如果TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时内核就选择一个临时端口,这对客户来说是正常的,服务器应该调用众所周知的端口
  2. 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,务必将其转化为网络字节序再赋给socket

实现

  该调用通过传递进来的文件描述符找到对应的socket结构,然后通过socket访问sock结构。操作sock进行地址的绑定。如果指定了端口检查端口的可用性并绑定,否则随机分配一个端口进行绑定。但是怎样获知当前系统的端口绑定状态呢?通过一个全局变量inet_hashinfo进行,每次成功绑定一个端口会都将该sock加入到inet_hashinfo的绑定散列表中。加入之后bind的系统调用已基本完成了。

3.listen函数

int listen(int sockfd, int backlog);
  1. 第一个参数即为要监听的socket描述字
  2. 第二个参数为相应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,其中:

  1. 首先看看这个包是不是一个已经建立好连接的sock中的包,通过__inet_lookup_established函数进行操作(一个连接通过源IP,目的IP,源PORT和目的PORT标识)。
  2. 失败的话可能是一个新的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 ;
}
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 ;
}
}
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
  1. 如果是一个已建立连接的sock,调用tcp_rcv_established函数进行相应的处理。
  2. 如果是一个正在监听的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;
}
  1. 首先调用inet_csk_search_req查找在半连接队列中是否已经存在对应的request_sock。有的话说明这个请求连接已经存在,调用tcp_check_req处理第三次握手的情况,当sock的状态从SYN_RCV变迁到ESTABLISHED状态时,连接建立完成。需要将该request_sock从request_sock_queue队列中的listen_opt半连接队列取出,放入全连接队列等待进程调用accept取走,同时是request_sock指向一个新建的sock并返回。
  2. 没有的话调用inet_lookup_established从已经建立连接sock中查找,如果找到的话说明这是一条已经建立的连接,当该sock不处于timewait将sock返回状态时将sock返回,否则返回NULL。
  3. 当上述两种情况都失败了,表示这是一个新的为创建的连接,直接返回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)。
注意:

  1. 若TCP客户没75s有收到SYN分节的响应(时隔6s发一个),返回ETIMEDOUT,
  2. 对客户的SUN分节的相应是RST,表明服务器主机在我们指定的端口无进程等待,或服务器没有运行,硬错误,返回ECONNREFUSED
  3. 若客户发生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返回的套接字来完成与客户的通信。
注意:

  1. accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字
  2. 连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号

需要区分两种套接字

  1. 监听套接字:监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
  2. 连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而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(-,&stat,WNOHANG))>)
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))>)
write(sockfd,buffer,n); //处理被中断的慢系统调用
if(n<&&errno==EINTR)
goto again;
else if(n<)
err_sys("str_echo:read error");
} int main()
{
int listenfd=socket(AF_INET,SOCK_STREAM,); 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()
{
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<)
if(errno==EINTR)
continue;
else
err_sys("accept error"); pid_t cpid;
//fork之后listenfd和connfd的引用计数都为2
if((cpid=fork())==)
{
close(listenfd);//关闭监听套接字,时listenfd计数一直为一
str_echo(connfd);
close(connfd);
exit();
}
close(connfd);//新的客户由子进程提供服务,父进程可以关掉已连接套接字
}
exit();
}

客户代码

阻塞的客户端

#include "unp.h"
#include "my_err.h" void str_cli(FILE *fp,int sockfd)
{
char buffer[MAXLINE];
int stdineof=; fd_set rest;
FD_ZERO(&rest); int n;
while()
{
if(stdineof==)
FD_SET(fileno(fp),&rest); FD_SET(sockfd,&rest);//把socked描述符加入rest集合 int maxfd=max(fileno(fp),sockfd)+;
/*
*客户端等待可读:标准输入可读或是套接字可读,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))==)
if(stdineof==)
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))==)
{
stdineof=;
/*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!=)
err_quit("please input tcplicent <IP-address"); int sockfd=socket(AF_INET,SOCK_STREAM,); struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,argv[],&servaddr.sin_addr);//把assic转换为二进制 connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); str_cli(stdin,sockfd);
exit();
}

connect出错

  1. 若TCP客户端没有收到syn分节的响应,则返回ETIMEOUT错误;调用connect函数时,内核发送一个syn,若无响应则等待6s后再发送一个,若仍然无响应则等待24s后在发送一个,若总共等待75s后仍未收到响应则返回本错误;
  2. 若对客户的syn响应是rst,则表明该服务器在我们指定的端口上没有进程在等待与之连接,这是一种硬错误,客户一收到rst马上返回ECONNREFUSED错误(产生RST三个条件:1.目的地为某个端口的SYN到达而端口上没有正在监听的服务器2.TCP想取消一个已有的连接3.TCP收到一个根本不存在的连接上的分节);
  3. 若客户发送的syn在中间的某个路由器上引发了目的不可达icmp错误,则认为是一种软错误。客户主机内核保存该消息,并按照第一种情况的时间间隔继续发送syn,咋某个规定时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或者ENETUNREACH错误返回给进程;

accept返回前连接中止

  在比较忙的服务器中,在建立三次握手之后,调用accept之前,可能出现客户端断开连接的情况;如,三次握手之后,客户端发送rst,然后服务器调用accept。posix指出这种情况errno设置为CONNABORTED;注意Berkeley实现中,没有返回这个错误,而是EPROTO,同时完成三次握手的连接会从已完成队列中移除;在这种情况下,如果我们用select监听到有新的连接完成,但之后又被从完成队列中删除,此时如果调用阻塞accept就会产生阻塞;

解决办法:

  1. 使用select监听套接字是否有完成连接的时候,总是把这个监听套接字设置为非阻塞;
  2. 在后续的accept调用中忽略以下错误,EWOULDBLOCK(Berkeley实现,客户中止连接), ECONNABORTED(posix实现,客户中止连接), EPROTO(serv4实现,客户中止连接)和EINTR(如果有信号被捕获);

服务器进程终止(崩溃)

  在客户端和服务器端建立连接之后,使用kill命令杀死服务器进程,进程终止会关闭所有打开的描述符,这导致了其向客户端发送了一个FIN,而客户端则响应了一个ack,这就完成了tcp连接终止的前半部分,只代表服务器不在发送数据了;但是客户端并不知道服务器端已经终止了,当客户端向服务器写数据的时候,由于服务器进程终止,所以响应了rst,如果我们使用select等方式,能够立即知道当前连接状态;如下:

  1. 如果对端tcp发送数据,那么套接字可读,并且read返回一个大于0的值(读入字节数);
  2. 如果对端tcp发送了fin(对端进程终止),那么该套接字变为可读,并且read返回0(EOF);
  3. 如果对端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=;
tval.tv_usec=; fd_set set,wset;
FD_ZERO(&set);
FD_ZERO(&wset);
string s;
while()
{
FD_SET(fileno(stdin),&set);
FD_SET(sockfd,&set);
FD_SET(sockfd,&wset); //客户端对应两个输入,套接字和用户输入,他不能单纯的阻塞在某个中断
//上,应该阻塞在任何一个中断上
int maxfd=max(fileno(stdin),sockfd)+;
int res=select(maxfd,&set,&wset,NULL,&tval);
if(res<=)
{
cerr<<"connect time out"<<endl;
close(sockfd);
exit();
}
/*
* socket描述符只可写,连接成功
* 若即可读又可写,分为两种情况:
* 第一种:出错,因为可能是connect连接成功后远程主机断开连接close(socket)
* 第二种:连接成功,socket读缓冲区得到了远程主机发送的数据,根据
* connect连接成功后errno的返回值来判定,或通过getsockopt函数返回值
* 来判断,但linux下getsockopt始终返回0,错误的情况下应返回-1
*/
int n=;
if(FD_ISSET(fileno(stdin),&set))
{
if((n=read(fileno(stdin),(void*)s.c_str(),))==)
{
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(),);
sleep();
write(sockfd,(void *)(s.c_str()+),n-);
}
}
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();
}
if((n=read(sockfd,(void *)s.c_str(),))>)
write(fileno(stdout),(void *)s.c_str(),n);
if(n==)//2.服务器端进程关闭,客户会收到服务器的一个RST
{
cerr<<strerror(errno)<<endl;
exit();
}
}
}
return ;
} int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,);
//禁用nagle算法
const char opt=;
setsockopt(sockfd,IPPROTO_TCP,TCP_NODELAY,&opt,sizeof(opt)); sockaddr_in servadddr;
memset(&servadddr,sizeof(servadddr),);
servadddr.sin_family=AF_INET;
servadddr.sin_port=htons();
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=;
l.l_linger=;
setsockopt(sockfd,SOL_SOCKET,SO_LINGER,&l,sizeof(l));
fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFL,)|O_NONBLOCK);
//res==0连接成功
//==-1开始三次握手但未完成
if(res==-)
{
if(errno!=EINPROGRESS)//表示正在试图连接,不能表示连接失败
{
//oeration now progress:套接字为非阻塞套接字,且原来的连接未完成
cout<<strerror(errno)<<endl;
exit();
}
/**
* 也可以在此处处理select连接
*/
}
cli_echo(sockfd);
close(sockfd);
return ;
}

服务器

/*************************************************************************
> 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(-,&stat,WNOHANG))>)
cout<<"child:"<<pid<<"terminated"<<endl;
return ;
}
void str_echo(int fd)
{
ssize_t n=;
char buf[];
again:while((n=read(fd,buf,))>)
write(fd,buf,n);
//处理中断系统调用错误
if(n<&&errno==EINTR)
goto again;
else if(n<&&errno==ECONNRESET)//1.
{
cerr<<"reset by perr"<<endl;
exit();
}
else
cerr<<"read error"<<endl;
} int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,);
int keepidle=;
setsockopt(sockfd,SOL_SOCKET,SO_KEEPALIVE,(void *)&keepidle,sizeof(keepidle));
sockaddr_in cliaddr,servaddr;
memset(&servaddr,sizeof(servaddr),);
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons();
servaddr.sin_addr.s_addr=htonl();//INADDR_ANY bind(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); listen(sockfd,);
signal(SIGCHLD,header);
while()
{
socklen_t len=sizeof(cliaddr);
//1.模拟较忙的服务器
//sleep(10);//完成三路握手后客户发送RST(复位)
conn:int connfd=accept(sockfd,(sockaddr *)&cliaddr,&len);
//处理被中断的系统调用,因为在阻塞于某个中断时,这时候进来一个
//信号,执行信号处理函数返回后系统调用会返回EINTR
if(connfd<)
{
if(errno==EINTR)
goto conn;
else if(errno==ECONNABORTED)
{
cerr<<"accept:connect reset by peer"<<endl;
exit();
}
}
else
{
pid_t pid;
if((pid=fork())==)
{
close(sockfd);
char des[];
//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();
}
close(connfd);
}
}
return ;
}

  在这些基础的socket API中,accept,connect,send,recv都可能是阻塞的,但是可以把他们编程非阻塞;

  1. 对于accept,send,recv来说,设置为非阻塞时errno返回值为-1且通常设置为EAGAIN(再来一次)或EWOULDBLOCk(期望阻塞)
  2. 对于connect,errno被置为EINPROGRESS(在处理中)

socket编程---TCP的更多相关文章

  1. 3、linux下Socket编程-TCP/UDP

    1.什么是Socket 网络的 Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符.Socket也具有一个类似于打开文件的函数调用Socket(),该函数返 回一个整型的Socke ...

  2. Socket编程 Tcp和粘包

    大多数程序员都要接触网络编程,Web开发天天和http打交道.稍微底层一点的程序员,就是TCP/UDP . 对程序员来说,Tcp/udp的核心是Socket编程. 我的浅薄的观点---------理解 ...

  3. Java套接字Socket编程--TCP参数

    在Java的Socket中,主要包含了以下可设置的TCP参数. 属性 说明 默认值 SO_TIMEOUT 对ServerSocket来说表示等待连接的最长空等待时间; 对Socket来说表示读数据最长 ...

  4. linux下socket编程-TCP

    网络字节序 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出 ...

  5. SOCKET 编程TCP/IP、UDP

    TCP/IP 资源:http://download.csdn.net/detail/mao0514/9061265 server: #include<stdio.h> #include&l ...

  6. 网络Socket编程TCP协议例子

    1.单线程TCP服务端 public class TcpChatServer { private Integer port=8000; private ServerSocket serverSocke ...

  7. socket 编程 TCP 实现简单聊天功能【转】

    转自:http://blog.csdn.net/liujia2100/article/details/9006479 版权声明:本文为博主原创文章,未经博主允许不得转载. 各个主要函数的功能: .so ...

  8. socket编程 TCP 粘包和半包 的问题及解决办法

    一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系. 粘包 为x.5个包 半包 为0.5个包 由于网络原因 一次 ...

  9. python基础之socket编程(TCP三次握手和四次挥手)

    TCP协议中中的三次握手和四次挥手 建立TCP需要三次握手才能建立,而断开连接则需要四次握手.整个过程如下图所示: 先来看看如何建立连接的. 首先Client端发送连接请求报文,Server段接受连接 ...

随机推荐

  1. Exception.StackTrace

    Exception中的StackTrace属性 执行堆栈跟踪在给定时刻正在执行的所有方法. 对方法调用的跟踪称为堆栈跟踪. 堆栈跟踪列表提供了一种循着调用堆叠跟踪到方法中异常发生处行号的手段.Stac ...

  2. jquery获取服务器控件Label的值

    首先引入js文件:<script type="text/javascript" src="js/jquery-1.8.3.js"></scri ...

  3. 下载百度网盘破解 获得 所下载视频URL 粘贴到thunder

    Chrome:方法1. 进入谷歌商城,搜索baidudl.安装即可. 方法2. 下载baidudl.zip.解压获得baidudl文件夹.进入chrome://extensions/,勾选右上角Dev ...

  4. bin log、redo log、undo log和MVVC

    logs innodb事务日志包括redo log和undo log.redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作. undo log不是redo log的逆向过 ...

  5. 关闭pm2

    先查找ID pm2 status 然后 pm2 stop id pm2 delete id

  6. 快捷方式控制台调试each这种方法的时候怎么停

    1.ctrl +' 2.当遇到angular.each的时候ctrl + ; 3.进入之后,还是先ctrl+'; angular中: 当遇到forEach之后,又一次ctrl+;就回到你的each之后 ...

  7. ListBox的虚拟可视化技术

    在ListBox中承载大量的数据项时,可采用虚拟可视化技术来提高控件显示数据的性能.如下代码: <ListBox.ItemsPanel>                    <It ...

  8. hdu3951巴什博弈变型

    参考博客:http://blog.csdn.net/sun897949163/article/details/50609070 特判一下m=1的情况,然后m!=1时,无论对手取多少,我只要取的让这条链 ...

  9. 2-13 搭建LAMP环境并部署Ucenter和Ucenter-home网站

    环境: VMware Virtual Machine : XGan63.cn IP: 192.168.31.63 (Bridge) 已配置本地yum源 ---> /mnt 已配置网络yum源 - ...

  10. 235.236. Lowest Common Ancestor of a Binary (Search) Tree -- 最近公共祖先

    235. Lowest Common Ancestor of a Binary Search Tree Given a binary search tree (BST), find the lowes ...