对TCP套接字调用connect会激发三次握手,例如以下:
client是主动打开连接的一端,会发送第一个SYN分节,然后等待确认,此时连接状态为SYN_SENT,当收到服务端的确认后连接建立,状态变为ESTABLISHED;
server是被动打开连接的一端,调用listen导致套接字从CLOSED状态变为LISTEN状态,当收到来自client的SYN分节以后状态变为SYN_RCVD,然后发送第二个SYN分节,等待client的确认,收到client的确认以后连接建立,状态变为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,则表示该server主机在我们指定的port上面没有进程在等待与之连接,比如server进程没执行,客户收到RST就立即返回ECONNREFUSED错误;
3. 若客户发出的SYN在中间的某个路由上引发了一个“destination unreachable”(目的不可达)ICMP错误,客户主机内核保存该消息,并按1中所述的时间间隔发送SYN,在某个规定的时间(4.4BSD规定75s)仍未收到响应,则把保存的ICMP错误作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
若connect失败则该套接字不再可用,必须关闭,我们不能对这种套接字再次调用connect函数。
在每次connect失败后,都必须close当前套接字描写叙述符并又一次调用socket。
我们重现一下这些错误:
首先看下下面系统定义:
#define ENETUNREACH 51 /* Network is unreachable */
#define ETIMEDOUT 60 /* Operation timed out */
#define ECONNREFUSED 61 /* Connection refused */
client:
<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("绑定port失败,errno = %d\n",errno);
return -1;
}
else
{
printf("绑定port成功\n");
}
// listen(listenfd, 20);
sleep(60*5);
return 0;
}
</span>
连接失败,errno = 60
不执行服务端,直接执行client会复现另外一种情况,直接打印例如以下信息:
连接失败,errno = 61
关掉wifi,直接执行会复现第三种情况,直接打印例如以下信息:
连接失败,errno = 51
这里有个疑问,要是服务端打开屏蔽listen的那行代码会怎么样,再执行,client打印:
连接成功
我们服务端没有调用accept代码呀,这是由于调用listen方法后,内核为不论什么一个给定的监听套接字维护两个队列:未完毕连接队列和已完毕连接队列例如以下图所看到的;当客户SYN到达时,假设队列是满的,TCP就忽略该分节,但不会发送RST;当进程调用accept时,已完毕队列的对头项将返回给进程,假设队列是空,则堵塞(套接字默认堵塞);也就是说仅仅要我调用了listen方法后,服务端就打开了三次握手的开关,可以处理来自client的SYN分节了,仅仅要三次握手完毕,client就会connect成功,而跟服务端调用accept没不论什么关系,accept仅仅是去取已完毕连接队列的对头项。
如图为TCP监听套接字的两个队列:
參考:
《UNIX Network ProgrammingVolume 1, Third Edition: TheSockets Networking API》