客户端发起连接的过程
第一步建立一个套接字,不一样的是客户端需要调用connect发起请求。
connect
客户端和服务器端的连接建立,是通过connect函数完成的。这是connect的构建函数:
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
- sockfd
连接套接字,通过前面讲述的socket函数创建 - servaddr、addrlen
指向套接字地址结构的指针和该结构的大小。
套接字地址结构必须含有服务器的IP地址和端口号。
客户在调用函数connect前不必非得调用bind函数,如果需要,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,那么调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回。其中出错返回可能有以下几种情况:
三次握手无法建立,客户端发出的SYN包没有任何响应,于是返回TIMEOUT错误。这种情况比较常见的原因是对应的服务端IP写错。
客户端收到了RST(复位)回答,这时候客户端会立即返回CONNECTION REFUSED错误。这种情况比较常见于客户端发送连接请求时的请求端口写错,因为RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器(如前所述);TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。
客户发出的SYN包在网络上引起了"destination unreachable",即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。
根据不同的返回值,我们可以做进一步的排查。
著名的TCP三次握手: 这一次不用背记
你在各个场合都会了解到著名的TCP三次握手,可能还会被要求背下三次握手整个过程,但背后的原理和过程可能未必真正理解。我们刚刚学习了服务端和客户端连接的主要函数,下面结合这些函数讲解一下TCP三次握手的过程。这样我相信你不用背,也能根据理解轻松掌握这部分的知识。
这里我们使用的网络编程模型都是阻塞式的。所谓阻塞式,就是调用发起后不会直接返回,由操作系统内核处理之后才会返回。 相对的,还有一种叫做非阻塞式的,我们在后面的章节里会讲到。
TCP三次握手
服务器端通过socket,bind和listen完成了被动套接字的准备工作,被动的意思就是等着别人来连接,然后调用accept,就会阻塞在这里,等待客户端的连接来临;客户端通过调用socket和connect函数之后,也会阻塞。接下来的事情是由操作系统内核完成的,更具体一点的说,是操作系统内核网络协议栈在工作。
客户端的协议栈向服务器端发送了SYN包,并告诉服务器端当前发送序列号j,客户端进入SYNC_SENT状态;
服务器端的协议栈收到这个包之后,和客户端进行ACK应答,应答的值为j+1,表示对SYN包j的确认,同时服务器也发送一个SYN包,告诉客户端当前我的发送序列号为k,服务器端进入SYNC_RCVD状态;
客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为k+1;
应答包到达服务器端后,服务器端协议栈使得accept阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入ESTABLISHED状态。
形象一点的比喻是这样的,有A和B想进行通话:
A先对B说:“喂,你在么?我在的,我的口令是j。”
B收到之后大声回答:“我收到你的口令j并准备好了,你准备好了吗?我的口令是k。”
A收到之后也大声回答:“我收到你的口令k并准备好了,我们开始吧。”
可以看到,这样的应答过程总共进行了三次,这就是TCP连接建立之所以被叫为“三次握手”的原因了。
TCP三次握手建立连接的过程中,内核通常会为每一个LISTEN状态的Socket维护两个队列:
- SYN队列(半连接队列):这些连接已经接到客户端SYN
- ACCEPT队列(全连接队列):这些连接已经接到客户端的ACK,完成了三次握手,等待被accept系统调用取走。
比如 Tomcat 的Acceptor负责从ACCEPT队列中取出连接,当Acceptor处理不过来时,连接就堆积在ACCEPT队列中,这个队列长度也可以通过参数设置。
总结
- 服务器端通过创建socket,bind,listen完成初始化,通过accept完成连接的建立
- 客户端通过创建socket,connect发起连接建立请求