一个TCP程序实例
客户端:
①sockfd = socket(AF_INET, SOCK_STREAM, 0);
创建套接字AF_INET表示网际,SOCK_STREAM表示字节流,这两个加在一起就表示TCP套接字
②struct sockaddr_in servaddr;//声明一个网际套接字地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);//绑定服务器端口
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);//绑定服务器ip地址(先将点分十进制数串转换成32位二进制)
这几步的目的是转件目标地址,表示该套接字应该往哪个服务器上发。
(这两歩中间可选调用bind函数,一般不调用时,内核会确定源IP地址,并选择临时端口)
③connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
应用在sockfd上,使其与servaddr指定的地址建立连接
connect失败时,返回-1,此时必须要关闭当前socket,并重新生成,重新进行connect
④在一个循环中调用
write(sockfd, sendbuff,strlen(sendbuff));//将请求信息写入套接字
read(sockfd, charbuffer, maxLine);//读取套接字中服务器返回的结果
⑤调用exit(0),进程终止的时候会关闭所有的描述符,所以tcp套接字也被关闭
服务端:
①listenfd = socket(AF_INET, SOCK_STREAM, 0);
创建套接字,AF_INET表示网际,SOCK_STREAM表示字节流,这两个加在一起就表示TCP套接字
②struct sockaddr_in servaddr;//声明一个网际套接字地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);//绑定服务器端口
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//
这几步指定监听字的端口
③bind(listenfd, (SA *)&servaddr, sizeof(servaddr));//将端口与套接字进行绑定;
在客户端中,可以不用调用bind函数,因为内核会为该套接字分配临时端口,如果需要获知该端口号,可以调用getsockname函数获取;但是在服务器中,是必须要指定一个端口的,因为需要众所周知端口给客户端提供服务。
④listen(listenfd, LISTENQ);
调用这个函数之后,该套接字就会转换成监听套接字(内核中接收外来连接)
(在调用socket之后产生的套接字为主动套接字,是一个将调用connect发起连接的套接字,使用listen函数之后,该套接字会变成一个被动套接字,指示内核应该接受指向该套接字的外来请求);第二个参数为最大连接个数
socket-----bind-----listen是服务器监听的正常三步骤
当有一个客户端请求到来之后(SYN信号到达),会调用三次握手建立连接(自动的过程)。
⑤在一个循环体中:
connfd = accept(listenfd, (SA *) NULL, NULL);
accept函数其实是等待连接,服务器调用该函数之后会投入到睡眠直到某个客户端连接到达并被内核接受,连接建立之后执行accept返回,返回一个已连接描述符connfd(其实内部创建了已连接套接字);
该函数后面两个参数用来返回已连接对端进程的协议地址,如果不需要,则置NULL。
针对每一个客户端连接请求,都会返回一个已连接描述符
read(connfd, reqbuff,MAXLINE);//读取请求信息
write(connfd, buff, strlen(buff));//将反馈信息写入已连接描述符
上面的两步互不干扰,因为TCP是全双工的。
close(connfd);//关闭该连接,对于非并发服务器,调用close函数之后会发FIN,执行四次挥手
对于并发服务器(fork产生子进程进行处理),调用close之后,并不一定四次挥手,只是对相应的套接字描述符的计数减一,当减到0的时候才执行挥手。(当然,可以shutdown函数强行发送FIN)
并且,close关闭读和写两个端口,如果只需要关闭一端,就要调用shutdown函数:
int shutdown(int sockfd, int howto);
howto取值:---SHUT_RD:关闭读
---SHUT_WR:关闭写
---SHUT_RDWR:关闭读和写
TCP的11个状态转换:
CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对 方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
对于一个TCP连接:
客户端状态:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服务器状态:
CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
面试题:为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为: 虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到 ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于 LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的 ACK报文。
TIME_WAIT状态的套接字过多怎么办?
打开重用和快速回收的两个参数,或者修改TIME_OUT时间。
(1)connect请求连接的时候,只有建立连接或者是出错的时候才返回,这样可能会导致长时间阻塞的问题,解决方法:①使用定时器②使用select复用设置非阻塞
(2)keepalive 是什么东西?如何使用?
keepalive,是在TCP中一个可以检测死连接的机制。
.如果主机可达,对方就会响应ACK应答,就认为是存活的。
.如果可达,但应用程序退出,对方就发RST应答,发送TCP撤消连接。
.如果可达,但应用程序崩溃,对方就发FIN消息。
.如果对方主机不响应ack, rst,继续发送直到超时,就撤消连接。默认二个小时。
(3)关于RST信号
RST标示复位、用来异常的关闭连接。一般tcp连接出现错误都会发复位报文段。
----- 发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓冲区中的包,发送RST。
----- 而接收端收到RST包后,也不必发送ACK包来确认。
什么时候发送RST包?
-----建立连接的SYN到达某端口,但是该端口上没有正在监听的服务。
-----异常终止一个连接,使用RST代替FIN释放(此时排队数据可能没有发送完毕,所以是异常释放)
-----检测半打开的连接
-----TCP收到了一个根本不存在的连接上的分节。
-----请求超时。 使用setsockopt的SO_RCVTIMEO选项设置recv的超时时间。接收数据超时时,会发送RST包。
(4)socket什么时候可读(也就是执行read的时候不会阻塞)?
------正常情况下,监听套接字接收到对方的connect请求,并且已经完成连接,这个时候这个监听套接字可读
------socket接收缓冲区已接收字节数大于等于socket限制,也就是有数据可以读
------连接的读一半关闭,即对方发FIN,读会直接返回0
------异常情况下,返回-1
(5)TCP的保活定时器
保活:启动服务器和客户端的连接之后,过了很长时间都不会断,甚至中间路由器崩溃重启也不会断。
另一端崩溃
keepalive机制在重复发送探测包到一定次数后报错,由TCP转换为“连接超时”
另一端崩溃而重启
客户端telnet到服务器后,我们拔掉服务器网线重启服务器,服务器重启好后我们在telnet客户端上输入命令。服务器重启后丢失了以前连接的所有信息,此时服务器收到来着telnet客户端的命令,不知道此连接的信息,于是TCP就以复位作为应答来结束TCP连接。
另外一端不可达
拔掉网线,模拟中间路由器崩溃,keepalive探测的时候会引起ICMP差错“不可达--没有到达主机的路由”反馈给主机。
tcp报文头:
URG: 标识紧急指针是否有效
ACK: 标识确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST: 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN: 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段