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

错误信息:

  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;
};
View Code

  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 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;
}
View Code
  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;
}
View Code
  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(-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出错

  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=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都可能是阻塞的,但是可以把他们编程非阻塞;

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

posted on 2018-10-26 21:32  tianzeng  阅读(452)  评论(0编辑  收藏  举报

导航