-->

TCP协议


TCP协议

TCP全称(Transmission Control Protocol),传输控制协议。

TCP是面向连接的、可靠地。需要注意的是TCP是需要连接的所以是一对一的,不能使用广播或者组播。

  • TCP可靠性的描述

    1. TCP必须恢复来自互联网通信系统的被损坏、丢弃、复制或者无序交付的数据。
    2. 通过每8位字节分配一个序列号实现发送,并且还需要收到接收方的应答信号(ACK)
    3. 当超时时,没有响应数据,那么会发送方会从新发送
    4. 接收段使用序列号用于对可能无序接收段进行正确排序,以便于消除重复段
    5. 数据损坏的处理方法是给每个传输的段添加校验和,在接收端检查,然后将损坏的段丢弃。
  • TCP的数据头

需要注意的是TCP的数据头最小是24个字节(没有数据)。

image

主要说明中间黄色的部分

    • ACK:响应信号acknowledge
    • RST:复位信号
    • SYN:同步信号(建立连接请求)synchronize
    • FIN:完成信号(结束信号)finish

在使用TCP之前,可以先了解一下TCP的握手机制和挥手机制,在TCP建立连接的时候他做了这个动作

  • 握手

image

  • 挥手

image

  • TCP的函数接口

  • 创建套接字

int socket(int domain, int type, int protocol);
//第一个参数是域,使用这个宏AF_INET就是IPV4的
//第二个参数是类型UDP选择SOCK_DGRAM,TCP选择SOCK_STREAM
//第三个参数选择0,系统会自动帮你匹配所用的协议

//返回值,成功返回套接字文件的描述符,失败返回-1
  • 连接服务器(客户端)

需要注意的是,TCP是连接的协议,所以客户端不需要绑定。

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//第一个参数是套接字文件的描述符
//第二个参数是,需要链接的服务器的端口以及地址,他需要这个结构体但是一般都使用下面这个结构体然后强转为这个结构体类型,需要自己填
struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port; /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
};
//上面这个结构体第一个成员是地址家族,需要填写的值是AF_INET
//第二个成员是端口字节序,绑定自己程序的端口,以便于对方传输数据查找到这个程序
//第三个参数是结构体,这个结构体只有一个成员,也就是在下面,下面这个结构体也是需要一个地址字节序。

/* Internet address. */
struct in_addr {
               uint32_t       s_addr;/* address in network byte order */
};
//第三个参数是第二个参数的长度

//返回值,成功返回0,失败返回-1。
  • 发送数据

使用tcp协议发送数据,可以使用一下几个函数

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//第一个参数是套接字文件的描述符
//第二个参数是需要发送的缓冲区
//第三个参数是发送字节的长度
//第四个参数是标志位,不用就是0

//返回值,返回接收字节的大小,失败返回-1

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
//第一个参数是套接字文件描述符
//第二个参数是发送的缓冲区(注意不要超过1500个字节)
//第三个参数是发送字节的长度
//第四个参数是标志位,一般都填0
//第五个参数是一个结构体类型的指针,需要的是他需要这个结构体但是一般都使用下面这个结构体然后强转为这个结构体类型,需要自己填
struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port; /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
};
//上面这个结构体第一个成员是地址家族,需要填写的值是AF_INET
//第二个成员是端口字节序,绑定自己程序的端口(自己定),以便于对方传输数据查找到这个程序
//第三个参数是结构体,这个结构体只有一个成员,也就是在下面,下面这个结构体也是需要一个地址字节序,绑定自己主机的成员,以便于自己的端口绑定自己IP。

/* Internet address. */
struct in_addr {
               uint32_t       s_addr;/* address in network byte order */
};
//第六个参数是这个结构体的长度

//返回值,返回接收字节的大小,失败返回-1

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//第一个参数是套接字文件的描述符
//第二个参数是需要一个结构体
struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags (unused) */
};

//第三个参数是标志位,不用就是0

//返回值,返回接收字节的大小,失败返回-1

ssize_t write(int fd, const void *buf, size_t count);
//第一个参数是套接字文件的描述符
//第二个参数是需要发送的缓冲区
//第三个参数是发送字节的长度

//返回值,返回接收字节的大小,失败返回-1
  • 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//第一个参数是套接字文件的描述符
//第二个参数是需要接收的缓冲区
//第三个参数是接收字节的长度
//第四个参数是标志位,不用就是0

//返回值,返回接收字节的大小,失败返回-1

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
//第一个参数是套接字文件描述符
//第二个参数是接收的缓冲区
//第三个参数是接收字节的长度
//第四个参数是标志位,一般都填0
//第五个参数是一个结构体类型的指针,需要的是他需要这个结构体但是一般都使用下面这个结构体然后强转为这个结构体类型,不需要自己填,可以获取是谁发送的
struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port; /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
};
//上面这个结构体第一个成员是地址家族,需要填写的值是AF_INET
//第二个成员是端口字节序,绑定自己程序的端口,以便于对方传输数据查找到这个程序
//第三个参数是结构体,这个结构体只有一个成员,也就是在下面,下面这个结构体也是需要一个地址字节序,绑定自己主机的成员,以便于自己的端口绑定自己IP。

/* Internet address. */
struct in_addr {
               uint32_t       s_addr;/* address in network byte order */
};
//第六个参数是这个结构体的长度

//返回值是接收字节的个数,如果是失败返回-1

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//第一个参数是套接字文件的描述符
//第二个参数是需要一个结构体
struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags (unused) */
};
//第三个参数是标志位,不用就是0

//返回值,返回接收字节的大小,失败返回-1
       
ssize_t read(int fd, void *buf, size_t count);
//第一个参数是套接字文件的描述符
//第二个参数是需要接收的缓冲区
//第三个参数是接收字节的长度

//返回值,返回接收字节的大小,失败返回-1
  • 绑定IP和端口(服务器)

只有服务器才需要绑定端口

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//第一个参数是套接字文件的描述符
//第二个参数是一个结构体类型的指针,需要的是他需要这个结构体但是一般都使用下面这个结构体然后强转为这个结构体类型,需要自己填
struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port; /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
};
//上面这个结构体第一个成员是地址家族,需要填写的值是AF_INET
//第二个成员是端口字节序,绑定自己程序的端口,以便于对方传输数据查找到这个程序
//第三个参数是结构体,这个结构体只有一个成员,也就是在下面,下面这个结构体也是需要一个地址字节序,绑定自己主机的成员,以便于自己的端口绑定自己IP。

/* Internet address. */
struct in_addr {
               uint32_t       s_addr;/* address in network byte order */
};

//第三个参数是这个结构体的长度

//返回值,成功返回0,失败返回-1
  • 监听以及接收(服务器)

    • 在服务器中,需要监听数据,以及需要接受数据,需要接数据如下图所示
    • image
//设置队列最大等待个数
int listen(int sockfd, int backlog);
//第一个参数是套接字文件的描述符
//第二个参数是监听的数量

//返回值,成功返回0,失败返回-1

//接受函数(注意这个函数会阻塞,如果等待队列为空)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//第一个参数是套接字文件的描述符
//第二个参数需要一个这个类型的结构体,但是一般都把他定义为下面的类型,直接强制转换为参数的类型(可以接受数据的来源)
struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port; /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
};
//第三个参数是结构体的字节大小

//返回值,成功返回一个套接字文件描述符(只有使用这个套接字描述符,才能与之通信),失败返回-1
  • 接受的函数和发送的函数在上面

  • Example

注意需要让服务器先跑起来

//程序一,客户端
int main(int argc, char const *argv[])
{
    // 1.创建UDP套接字
    int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(1);
    }
    
    // 连接,服务器
    struct sockaddr_in server;

    server.sin_family = AF_INET;                     // 协议族,是固定的
    server.sin_port = htons(atoi("60000"));      // 目标端口,必须转换为网络字节序
    server.sin_addr.s_addr = inet_addr("192.168.136.128"); // 服务器的地址
    socklen_t server_len = sizeof(server);
    
    connect(tcp_socket, (struct sockaddr *)&server, server_len);
    
    char send_data[128] = "hello";
    
    write(tcp_socket, send_data, strlen(send_data));
    
    return 0;
}

// 程序二,服务器
int main(int argc, char const *argv[])
{
    // 1.创建UDP套接字
    tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(1);
    }

    // 绑定,绑定本地端口,接收服务器的消息
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;               // 协议族,是固定的
    addr.sin_port = htons(atoi("60000")); // 目标端口,必须转换为网络字节序
    addr.sin_addr.s_addr = inet_addr("192.168.136.128");
    bind(tcp_socket, (struct sockaddr *)&addr, sizeof(addr);

    // 一次最多监听5个
    listen(tcp_socket, 5);
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    accept(tcp_socket, (struct sockaddr *)&client, &len);

	// 接收信息
    char data[128] = {0};
    read(ptcp_socket, data, sizeof(data));
	printf("%s\n",data);

    return 0;
}

TCP协议数据传输接收的缓冲区

使用图片的形式查看

  • 总过程

image

  • 第一步

image

  • 第二步

image

  • 第三部

image

  • 修改缓冲区大小

这个缓冲区的大小是可以修改的,一般只需要修改套接字文件的属性,大小是2304-425984,需要注意的是设置的缓冲区在内核是会加倍的,比如设置128,那么实际的缓冲区就是256。
使用setsockopt设置属性,getsockopt获取属性。

int main()
{
	// 1.创建UDP套接字
    tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(1);
    }
    
    int optval = 256;
    //设置,
    setsockopt(tcp_socket, SOL_SOCKET, SO_RCVBUF,(void *)&optval, strlen(optval));
}

设置最低水位线

一般而言,使用recv等函数接收数据时,这些函数都会阻塞,一般情况有两种原因

  • 第一种原因是,内核缓冲区大小为空,也就是没有数据接收。
  • 第二种原因是,设置了最低水位线,使得需要内核缓冲区有这么多个数据才会接收。例如,最低水位线设置了50个字节,如果有49个字节发送过来,那么recv之类的函数也是不会去读取的,会阻塞。

最低水位线,默认是1个字节,也可以以通过设置套接字的属性,



int main()
{
	// 1.创建UDP套接字
    tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(1);
    }
    
    int optval = 256;//最低水位线是256个字节
    //设置,
    setsockopt(tcp_socket, SOL_SOCKET, SO_RCVLOWAT,(void *)&optval, strlen(optval));
}
  • 需要注意的是,设置了最低水位线,只有到达了这个数据才会结束数据,其他的情况只能阻塞,那么如果有数据很紧急的话,系统就不能及时响应。

linux提供了一个带外数据,就是如果发送了紧急数据,那么内核会发送一个信号SIGURG,但是这个数据,只能使用send发送数据,一次最多发送一个字节数据。需要注意的是这个函数第四个参数需要填写MSG_OOB

  • 也可以设置接收超时机制
int main()
{
	// 1.创建UDP套接字
    tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(1);
    }
    
    struct timeval t;
    t.tv_sec = 2;		//2秒
    t.tv_usec = 0;		//0微秒
    //设置,
    setsockopt(tcp_socket, SOL_SOCKET, SO_RCVTIMEO,(void *)&optval, strlen(optval));
    //设置完成后recv之类的函数,就会阻塞两秒
}
posted @ 2024-06-13 21:19  wuju  阅读(41)  评论(0)    收藏  举报