Socket之TCP相关
客户端socket简单示例(以下都是针对TCP协议)
#define BUF_SIZE 100
int socket = socket(AF_INET,SOCK_STREAm,IPPROTO_TCP);
struct sockaddr_in sockAddr;
sockAddr.sn_famil = AF_INET;
sockAddr.sn_addr.s_addr = inet_addr(127.0.0.1);//处理网络字节序,大小端差异
sockAddr.sn_port = htons(1234);
connect(socket,(struct sockAddr *)&sockAddr, sizeof(sockAddr) );
char buffer[BUF_SIZE];
read(socket,buffer,sizeof(BUF_SIZE)-1);
close(socket);
服务端sockt简单示例
AF_INET:代表ipv4地址;SOCK_STREAM:数据传输方式,代表面向连接的数据传输方法;IPPROTO_TCP:指定协议
int socket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockAddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));//清理内存
sockAddr.sin_famil = AF_INET;
sockAddr.sin_addr.s_addr = inet_addr(127.0.0.1);
sockAddr.sin_port = htons(1234);
bind(socket,(struct sockAddr_in*)sockAddr,sizeof(sockAddr));//绑定socket描述符,把组合体特定地址分配给socket。
listen(socket,20);//socket是对应描述符,20 是可以排队的最大连接数
sickle_t clnt_addr_size = sizeof(clnt_addr);
int clinSocket = accpect(socket, (struct sockaddr*)&serv_addr,clnt_addr_size);//获取客户端的socket
char str[] = "你好,客户端";
write(socket, str,sizeof(str));
close(clinSocket);
close(socket);
客户端流程
- 客户端创建socket,先调socket函数创建个socket返回socket描述符,它存在于协议族空间中没有具体地址。
- 声明结构体sockAddr_in,赋值地址族或称协议域,IP地址,端口,用来识别主机中的进程。
- conect连接服务器(这里开始三次握手连接);
- 然后可以使用下面函数进行发送接收数据了
- read()/write()
- recv()/send()
- readv()/writev()
- recvmsg()/sendmsg()
- recvfrom()/sendto()
- close() 使用后可以关闭数据了(这里开始四次握手断开连接)。
服务端流程
- 客户端创建socket,先调socket函数创建个socket返回socket描述符,它存在于协议族空间中没有具体地址。
- 声明结构体sockAddr_in,赋值地址族或称协议域,IP地址,端口,用来识别主机中的进程。
- 调用bind()函数把一个地址族中的特定地址赋给socket,如:把AF_INET,SOCK_STREAM,IPPROTO_TCP组合赋值给socket。如果没有调用过bind()这个函数,客户端调用connect() 或者服务端调用listen()监听函数,系统会自动分配一个端口号和自身的IP地址组合,用于提供服务,而客户端就不用指定,这也是服务端代码要调bind()函数而客户端不用的原因。(客户端不用bind函数)。
- 调用listen(),开始监听上面创造的socket。
- 当socket处于listen状态,调用accept()程序就会进入阻塞状态,直到接受到客户端的connect()请求。accept() 会返回一个和客户端有关的socket描述字符,然后可以对这个字符进行各种读写操作,便是向客户端发送数据。
6.使用完成后便可以close关闭自己创建的socket和accept返回的socket
注:close是将socket从内存中清楚,还有种方式shutdown,用来断开连接,但是不会清除socket,函数原型int shutdown(int sock, int howto),howto参数表示断开方式。在linux下有以下几个方式。
- SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。其他平台稍有不同。
- SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
- SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以
- SHUT_RD 为参数,另一次以 SHUT_WR 为参数
三次握手建立连接
首先得有个服务端处于监听socket的状态,accept阻塞着线程等待客户端的请求。
下图是网上找的TCP数据报结构,握手时的数据包格式
TCP数据报结构
三次握手中,需要对TCP头部的这几个概念理解
- seq表示包的序号。
- ack来确认对方发送的某个序号seq收到了,采用ack = seq + 1的方式
- URG,ACK,PSH,PST,SYN,FIN这6个bit表示标志,SYN表示是同步连接数据包,FIN是否断连数据包。所以四次断连发送的就是FIN包。
第一次:客户端调用connect()向目标socket发起连接请求,这一步客户端socket会组织一个数据包比如:ACK标志位不设置,设置SYN标志位(Synchronous同步的意思,同步建立连接)标志位 =1,seq = x,进入SYN-SEND(同步已发送)状态,发送给服务端,此时connect()函数会进入阻塞状态,等待服务端的返回。
第一次握手数据包
第二次:服务端接受到客户端的请求和数据包后,组织数据包,SYN置1,ACK标志位(Acknowledge确认的意思)置1, 同时生成一个确认序号ack= x+1 ,同时随机选择seq = y进入SYN-RCVD(同步已发送)状态。
第二次握手数据包
第三次:客户端接受到了服务端的确认连接请求数据包,此时connect会返回,进入连接状,组织数据包:SYN标志位不设置,ACK置1,确认号ack = y+1,seq= x + 1(我第二次发给你包),再次发送给服务端,表示已经知道你已经收到我想连接的请求并且可以连接了。此时服务端收到数据后,进入已建立连接的状态,accept()函数会返回一个socket 描述符,即,我们在服务端进行读写操作的socket,这样连接就完成了。
第三次握手TCP数据包 三次握手连接示意图
注:
seq的作用是标识数据包序号,ack的作用是确认对方发送相应seq序号的数据包是否收到,把客户端发的seq加上1赋值给ack,然后返回给客户端表示已经收到你的连接请求,同时自己根据ack的值,再次组织下一个按序号应该发送的包,给seq赋值,发送给客户端,客户端收到数据后,查看ack是否是自己第一次发送数据包里的seq加上1的值,来确认服务端有没有收到自己的连接请求,然后客户端重新组织数据包,把服务端数据包里的seq加上1 赋值给自己的ack确认号,发送给服务端,表示我已经知道你收到我发送连接的请求了。
这个过程中若是收到服务的数据包ack并不是上次自己发送seq加上1,则会认为服务端没有收到数据,会重新组织数据再次发送,直到接到服务端包含正确的seq数据包或者等待数据包时间超时失效。
四次握手断开连接
-
客户端调用close()函数后,向服务端发送FIN数据包(设置了FIN标志位为1),客户端进入第一等待阶段。
-
服务器收到数据包后,检测到设置FIN标志位,知道要断开连接了,向客户端发送一个确认包,服务端进入关闭等待阶段。并不是立即关闭,只是先向客户端发送确认包,告诉客户端,我知道要断开了,准备些事情。此时,客户端收到确认包后,等待服务器准备完毕再次给自己发送数据包,这时客户端进入第二等待阶段。
-
过一段时间后,服务器准备完成,可以断开连接了,于时再次发送FIN包,告诉客户端,准备好了,可以断开连接了,这时服务端进入一个LAST_ACK的状态(猜测时只能接收一个ACK包的状态)。
-
客户端收到服务端的FIN数据包后,再次向服务端发送ACK包,告诉服务端,断开吧,就进入了TIME_WAIT状态(注意,此时客户端并不是立即断开连接,而是会等待一会,因为会存在数据包丢失的情况,若是服务端没有收到自己最后发送的数据包,则服务端还会向客户端发送FIN_2数据包,等待时间,与数据包在网络中的有效时限有关)。此时,服务端接受到数据后会关闭Socket进入Close状态。客户端等了一段时间后仍没有接到FIN_2的数据包,就会认为,服务端已经接到自己最后一次发送的数据包,这时客户端才会进入CLOSE的状态。
四次断连图
socket缓冲区以及阻塞模式
socket缓冲区
每个socket创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()和send() 并不是立即向网络中传输数据,而是先将数据写入缓冲区中,由TCP协议将数据发送到目标机器,一旦数据写入缓冲区,函数就可以成功返回,不管有没有到达目标机器,也不管他们何时被发送到网络,这些都是TCP负责的事情。
这些I/O缓冲区特性如下:
- I/O缓冲区在每个TCP套接字中单独存在
- I/O缓冲区在创建套接字时会自动生成
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据。
-
关闭套接字将丢失输入缓冲区中的数据
四次握手断连示意图.jpg
阻塞模式
对于TCP套接字,当使用write()/send()发送数据时:
- 首先会检查缓冲区,若缓冲区的剩余空间小雨要发送的数据时,write()/send()进入阻塞模式,将会等待数据发送到目标机器,腾出足够空间,才会唤醒write()/send()函数继续写入数据。
- 如果TCP协议正在向网络发送数据,那么输出缓冲区将会被锁定,不允许写入,write/send()函数写入数据。
- 如果要写入的数据大于缓冲区的最大长度,那么将会分批写入。
- 直到所有数据被写入缓冲区 write()/send() 才能返回。
当使用read和recv读取数据时。 - 手写会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
- 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能不一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到read()/recv()函数才会返回,否则就一直被阻塞。
- 直到读取到数据后 read()/recv()才会返回,否则就一直被阻塞。
所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。