• APUE学习--网络编程(3)


        本篇文章介绍TCP通信。

        上文提到传输层的两个协议TCP和UDP,UDP是无连接的已经介绍过,TCP是面向连接的,阐述建立连接和断开连接前先来看下TCP报文头的结构。

        报文头在linux的定义在/usr/include/netinet/tcp.h中:

    struct tcphdr
    {
        u_int16_t source;   //源端口号
        u_int16_t dest;      //目的端口号
        u_int32_t seq;       //32位的TCP报文序列号
        u_int32_t ack_seq; //32位的TCP报文确认序列号
        u_int16_t res1:4;   //保留位
        u_int16_t doff:4;   //首部长度
        u_int16_t fin:1;     //fin置1表示该报文用于申请断开TCP连接
        u_int16_t syn:1;   //syn置1表示该报文用于申请建立TCP连接
        u_int16_t rst:1;     //rst置1表示该报文用于申请重建TCP连接
        u_int16_t psh:1;    //psh置1表示该报文的优先级较高(用于发送紧急报文)
        u_int16_t ack:1;    //ack置1表示该报文具有确认的功能,此时确认序列号有效
        u_int16_t urg:1;    //urg置1使紧急指针有效
        u_int16_t res2:2;  //保留位(加上res1共6位)
        u_int16_t window; //窗口大小,用于流量控制
        u_int16_t check;   //tcp报文的校验和
        u_int16_t urg_ptr; //紧急指针(是一个偏移量),序列号到紧急指针之间的数据为紧急数据,紧急指针后的数据才是正常数据
    };
    


        可以看出来,TCP报文首部设计的功能要比UDP报文首部复杂的多(可靠自然带来大量的额外开销),在建立连接中我们比较关心的是32位的序列号、32位的确认序列号、SYN位、ACK位,通过下面的图形我们来看下TCP建立连接的三次握手过程(三次握手指的是三次报文的传输),其中发起连接的一端我们称为主动端,等待连接的一端我们称为被动端。


    第一次握手:主动端向被动端发送一个syn置1,序列号为x的一个tcp报文

    第二次握手:被动端向主动端回溯一个ack置1,确认序列号为x+1的tcp报文同时将该报文的syn置1并生成一个序列号y,以此使该报文又具有了发起连接的功能

    第三次握手:主动端再向被动端回溯一个ack置1,确认序列号为y+1的的确认报文,到此完成了tcp连接的建立。

    简单总结下:完成tcp建立连接需要两端都进行以此连接申请和申请的确认,但总会有一个主动端先申请建立连接。下面我们来看下如何通过函数调用来完成整个建立的阶段。

        先来看下被动端,有一个阶段是等待连接请求的阶段,通过listen()函数开启连接的监听等待:

    int listen(int sockfd, int backlog);

    参数sockfd是在哪个套接字上实现监听,backlog是最多能够建立几个连接(已经完成三次握手的)。listen()函数并不会阻塞等待,而是开启监听等待,开启后的等待过程是由内核的协议栈完成的,此时sockfd套接字已成为监听描述符,该描述符的可读条件变成有连接完成,当有连接完成时通过accept()函数来读出完成的连接并返回该TCP套接字描述符:

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    参数sockfd是监听描述符,addr是主动端的网络地址,addrlen是地址长度。该函数被调用后会阻塞至sockfd可读(即有连接完成),返回tcp套接字描述符。注意在TCP的被动端至少有两个套接字描述符,一个是监听描述符(用socket()创建并用listen开启监听状态),一个是tcp通信的描述符(由内核创建并由accept返回)。

        连接建立之后,套接字的通信双方已经确定,在通信时就不必对方的网络地址,直接使用read()/write()传输数据即可。

        TCP连接的被动端的函数调用过程:socket()创建监听描述符-->bind()绑定本地网络地址-->listen()开启监听状态-->accept()获得建立的tcp连接的套接字描述符-->read()/write()进行数据通信-->close()关闭套接字(监听描述符结束监听状态,tcp连接描述符断开TCP连接)。

        在来说下TCP的主动端,主动端使用connect()函数发起tcp连接的建立:

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    参数sockfd是套接字描述符(由socket()创建),addr是被动端的网络地址,addrlen是addr的长度。该函数将阻塞至内核完成三次握手,主动端只有一个描述符(无监听描述符)。

        TCP连接的主动端的函数调用过程:socket()创建套接字描述符-->connect()发起连接请求-->read()/write()进行数据通信-->close()关闭套接字(断开TCP连接)。

        下面我们看下断开连接的四次握手,从之前提到的函数调用过程中可以发现,无论是主动端还是被动段都可以发起断开连接,下面以主动端发起断开连接请求为例,被动端先发起是一样的。


    第一次握手:主动端向被动发送fin置1,序列号为x的断开连接报文

    第二次握手:被动端向主动端回溯一个ack置1,确认序列号为x+1的确认报文,主动端接到后为半关闭状态

    第三次握手:被动端过一端时间后再向主动端发送fin置1,序列号为y的断开连接报文

    第四次握手:主动端向被动端回溯一个ack置1,确认序列号为y+1的确认报文,此时连接为全关闭状态
       

        整个过程是在调用close()之后由内核完成,但close()并不阻塞。这样在编程时可能会出现的这种情况,被动端在绑定网络地址时出现地址已经被占用,过一段时间后才能绑定成功,原因就是上次的四次握手还未完成。解决办法一个使用setsockopt()函数设置地址可被重复绑定,具体操作如下:

    int on = 1;
    setsockopt(sockfd, SOL_SOCK, SO_REUSEADDR, &on, sizeof(on));

    该操作应该在socket()和bind()之间调用,其中SOL_SOCK表示是套接字的通用选项,SO_REUSEADDR表示的是地址重用选项,on=1表示开启。













  • 相关阅读:
    C# Unity依赖注入
    Spring学习总结
    .Net 上传文件和下载文件
    JavaWeb学习篇--Filter过滤器
    Struts2入门教程
    Ceph 时钟偏移问题 clock skew detected 解决方案--- 部署内网NTP服务
    Erasure Coding(纠删码)深入分析 转
    s3cmd : Add a config parameter to enable path-style bucket access 当ceph rgw使用域名时,需要支持 path-style bucket特性
    ceph rgw java sdk 使用域名访问服务时需要设置s3client的配置项 PathStyleAccess 为true, 负责将报域名异常
    直播流怎么存储在Ceph对象存储上? Linux内存文件系统tmpfs(/dev/shm) 的应用
  • 原文地址:https://www.cnblogs.com/dyllove98/p/3161475.html
Copyright © 2020-2023  润新知