首页 > 代码库 > 网络编程Socket之TCP之connect详解

网络编程Socket之TCP之connect详解

对TCP套接字调用connect会激发三次握手,如下:



客户端是主动打开连接的一端,会发送第一个SYN分节,然后等待确认,此时连接状态为SYN_SENT,当收到服务端的确认后连接建立,状态变为ESTABLISHED

服务器是被动打开连接的一端,调用listen导致套接字从CLOSED状态变为LISTEN状态,当收到来自客户端的SYN分节以后状态变为SYN_RCVD,然后发送第二个SYN分节,等待客户端的确认,收到客户端的确认以后连接建立,状态变为ESTABLISHED


三次握手中的两个SYN分节都会告诉对端本端在同一连接中发送数据的初始序列号,ACK的确认号是本端所期待的下一个序列号,SYN和FIN都占据一个字节的序列号空间;


SYN中携带的TCP选项:

MSS:告知对端本端在本连接中得每个TCP分节中愿意接受的最大数据量,发送端TCP使用接收端的MSS作为所发送分节的最大大小,我们可以通过TCP_MAXSEG套接字选项提取和设置这个TCP选项,TCP_MAXSEG选项原本是只读选项,4.4BSD限制应用进程只能减少其值,不能增加其值。

窗口规模选项:TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中,相应地字段占16位;SO_RCVBUF套接字选项影响这个TCP选项,套接字接收缓冲区中可用空间的大小限定了TCP通告对端的窗口大小;

时间戳选项:这个选项对于高速网络连接是必要的,它可以防止由失而复得的分组可能造成的数据损坏,这个失而复得是指由暂时的路由原因造成的迷途,路由稳定后又正常到达目的地,高速网络中32位的序列号很快就可能循环一轮重新使用,如果不用时间戳选项,失而复得的分组所承载的分节可能与再次使用相同序列号的真正分节发生混淆;


connect(套接字默认阻塞)出错返回的情况:

1. 调用connect时内核发送一个SYN分节,若无响应则等待6s后再次发送一个,仍无响应则等待24s再发送一个,若总共等了75s后仍未收到响应则返回ETIMEDOUT错误;

2. 若对客户的SYN的响应是RST,则表示该服务器主机在我们指定的端口上面没有进程在等待与之连接,例如服务器进程没运行,客户收到RST就马上返回ECONNREFUSED错误;

3. 若客户发出的SYN在中间的某个路由上引发了一个“destination unreachable”(目的不可达)ICMP错误,客户主机内核保存该消息,并按1中所述的时间间隔发送SYN,在某个规定的时间(4.4BSD规定75s)仍未收到响应,则把保存的ICMP错误作为EHOSTUNREACHENETUNREACH错误返回给进程。


connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。


在每次connect失败后,都必须close当前套接字描述符并重新调用socket


我们重现一下这些错误:

首先看下以下系统定义:

#defineENETUNREACH 51/* Network is unreachable */

#defineETIMEDOUT 60/* Operation timed out */

#defineECONNREFUSED 61/* Connection refused */


客户端:

<span style="font-size:12px;">int main(int argc, const char * argv[])
{

    struct sockaddr_in serverAdd;
    
    bzero(&serverAdd, sizeof(serverAdd));
    serverAdd.sin_family = AF_INET;
    serverAdd.sin_addr.s_addr = inet_addr(SERV_ADDR);
    serverAdd.sin_port = htons(SERV_PORT);
    
    int connfd = socket(AF_INET, SOCK_STREAM, 0);

    int connResult = connect(connfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
    if (connResult < 0) {
        printf("连接失败,errno = %d\n",errno);
        close(connfd);
        return -1;
    }
    else
    {
        printf("连接成功\n");
    }
    close(connfd);
    return 0;
}
</span>


服务端:

<span style="font-size:12px;">int main(int argc, const char * argv[])
{

    struct sockaddr_in serverAdd;
    struct sockaddr_in clientAdd;
    
    bzero(&serverAdd, sizeof(serverAdd));
    serverAdd.sin_family = AF_INET;
    serverAdd.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAdd.sin_port = htons(SERV_PORT);
    
    socklen_t clientAddrLen;
    
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1;
    setsockopt(listenfd,
               SOL_SOCKET, SO_REUSEADDR,
               (void *)&yes, sizeof(yes));
    
    if (listenfd < 0) {
        printf("创建socket失败\n");
        close(listenfd);
        return -1;
    }
    
    int bindResult = bind(listenfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
    if (bindResult < 0) {
        close(listenfd);
        printf("绑定端口失败,errno = %d\n",errno);
        return -1;
    }
    else
    {
        printf("绑定端口成功\n");
    }
    
//    listen(listenfd, 20);
    sleep(60*5);

    return 0;
}
</span>


先运行服务端,再运行客户端,客户端在休眠一段时间以后会复现第一个错误,客户端打印如下信息:

连接失败,errno 60

不运行服务端,直接运行客户端会复现第二种情况,直接打印如下信息:

连接失败,errno 61

关掉wifi,直接运行会复现第三种情况,直接打印如下信息:

连接失败,errno 51


这里有个疑问,要是服务端打开屏蔽listen的那行代码会怎么样,再运行,客户端打印:

连接成功

我们服务端没有调用accept代码呀,这是因为调用listen方法后,内核为任何一个给定的监听套接字维护两个队列:未完成连接队列和已完成连接队列如下图所示;当客户SYN到达时,如果队列是满的,TCP就忽略该分节,但不会发送RST;当进程调用accept时,已完成队列的对头项将返回给进程,如果队列是空,则阻塞(套接字默认阻塞);

也就是说只要我调用了listen方法后,服务端就打开了三次握手的开关,能够处理来自客户端的SYN分节了,只要三次握手完成,客户端就会connect成功,而跟服务端调用accept没任何关系,accept只是去取已完成连接队列的对头项。

如图为TCP监听套接字的两个队列:



参考:

UNIX Network ProgrammingVolume 1, Third Edition: TheSockets Networking API

网络编程Socket之TCP之connect详解