• 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
    ",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(在处理中)
  • 相关阅读:
    事务隔离级别
    OpenSessionInView
    图像平滑处理(滤波)
    [原]Nginx+Lua服务端合并静态文件
    检查数据倾斜分布
    SQL Server研究之统计信息—发现过期统计信息并处理具体解释
    GDALWarp设置GDALWarpOptions::dfWarpMemoryLimit过大时处理失败
    Android Studio 2.0 稳定版新特性介绍
    供应商和管理员查看供应商地址簿信息SQL
    Table AdvanceTable针对表内行禁用multipleSelection , singleSelection
  • 原文地址:https://www.cnblogs.com/tianzeng/p/9858855.html
Copyright © 2020-2023  润新知