美文网首页
Socket之TCP相关

Socket之TCP相关

作者: 分流替躺欧阳克 | 来源:发表于2019-06-03 18:01 被阅读0次

    客户端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);
    

    客户端流程

    1. 客户端创建socket,先调socket函数创建个socket返回socket描述符,它存在于协议族空间中没有具体地址。
    2. 声明结构体sockAddr_in,赋值地址族或称协议域,IP地址,端口,用来识别主机中的进程。
    3. conect连接服务器(这里开始三次握手连接);
    4. 然后可以使用下面函数进行发送接收数据了
    • read()/write()
    • recv()/send()
    • readv()/writev()
    • recvmsg()/sendmsg()
    • recvfrom()/sendto()
    1. close() 使用后可以关闭数据了(这里开始四次握手断开连接)。

    服务端流程

    1. 客户端创建socket,先调socket函数创建个socket返回socket描述符,它存在于协议族空间中没有具体地址。
    2. 声明结构体sockAddr_in,赋值地址族或称协议域,IP地址,端口,用来识别主机中的进程。
    3. 调用bind()函数把一个地址族中的特定地址赋给socket,如:把AF_INET,SOCK_STREAM,IPPROTO_TCP组合赋值给socket。如果没有调用过bind()这个函数,客户端调用connect() 或者服务端调用listen()监听函数,系统会自动分配一个端口号和自身的IP地址组合,用于提供服务,而客户端就不用指定,这也是服务端代码要调bind()函数而客户端不用的原因。(客户端不用bind函数)。
    4. 调用listen(),开始监听上面创造的socket。
    5. 当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头部的这几个概念理解

    1. seq表示包的序号。
    2. ack来确认对方发送的某个序号seq收到了,采用ack = seq + 1的方式
    3. 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数据包或者等待数据包时间超时失效。

    四次握手断开连接

    1. 客户端调用close()函数后,向服务端发送FIN数据包(设置了FIN标志位为1),客户端进入第一等待阶段。

    2. 服务器收到数据包后,检测到设置FIN标志位,知道要断开连接了,向客户端发送一个确认包,服务端进入关闭等待阶段。并不是立即关闭,只是先向客户端发送确认包,告诉客户端,我知道要断开了,准备些事情。此时,客户端收到确认包后,等待服务器准备完毕再次给自己发送数据包,这时客户端进入第二等待阶段。

    3. 过一段时间后,服务器准备完成,可以断开连接了,于时再次发送FIN包,告诉客户端,准备好了,可以断开连接了,这时服务端进入一个LAST_ACK的状态(猜测时只能接收一个ACK包的状态)。

    4. 客户端收到服务端的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()发送数据时:

    1. 首先会检查缓冲区,若缓冲区的剩余空间小雨要发送的数据时,write()/send()进入阻塞模式,将会等待数据发送到目标机器,腾出足够空间,才会唤醒write()/send()函数继续写入数据。
    2. 如果TCP协议正在向网络发送数据,那么输出缓冲区将会被锁定,不允许写入,write/send()函数写入数据。
    3. 如果要写入的数据大于缓冲区的最大长度,那么将会分批写入。
    4. 直到所有数据被写入缓冲区 write()/send() 才能返回。
      当使用read和recv读取数据时。
    5. 手写会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
    6. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能不一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到read()/recv()函数才会返回,否则就一直被阻塞。
    7. 直到读取到数据后 read()/recv()才会返回,否则就一直被阻塞。

    所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

    相关文章

      网友评论

          本文标题:Socket之TCP相关

          本文链接:https://www.haomeiwen.com/subject/ivrcxctx.html