[Linux 高并发服务器]TCP通信流程

Posted 鱼竿钓鱼干

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Linux 高并发服务器]TCP通信流程相关的知识,希望对你有一定的参考价值。

[Linux 高并发服务器]TCP通信流程

文章概述

该文章为牛客C++项目课程:Linux高并发服务器的个人笔记,记录了使用socket来实现TCP通信的相关流程

作者信息

NEFU 2020级 zsl
ID:fishingrod/鱼竿钓鱼干
Email:851892190@qq.com
欢迎各位引用此博客,引用时在显眼位置放置原文链接和作者基本信息

参考资料

感谢前辈们留下的优秀资料,从中学到很多,冒昧引用,如有冒犯可以私信或者在评论区下方指出

标题作者引用处
Linux高并发服务器牛客网贯穿全文,以此为基础
void* 是怎样的存在守望socket通用地址退化成void*补充

正文部分

TCP通信流程

摘自牛客网课程的pdf

TCP通信服务器端

1. 创建一个用于监听的socket

使用socket函数创建用于监听的套接字

int socket(int domain, int type, int protocol)
	-domain:协议族
		AF_INET:IPV4
		AF_INET6:IPV6
		AF_UNIX,AF_LOCAL:本地套接字用于进程间通信
	-type:使用的协议类型
		SOCK_STREAM:流式协议
		SOCK_DGRAM:报式协议
	-protocol:具体协议,一般写0
		SOCK_STREAM:流式协议默认tcp
		SOCK_DGRAM:报式协议默认udp
	-返回值:
		成功返回文件描述符
		失败返回-1
// 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

2. 将这个监听文件描述符和本地的IP和端口绑定

绑定之前需要先创建和初始化一个专用socket地址struct sockaddr_in存放本地IP和端口

sockaddr_in结构体

#include <netinet/in.h>
struct sockaddr_in
{
    sa_family_t sin_family;     /* __SOCKADDR_COMMON(sin_) */
    in_port_t sin_port;         /* Port number.  */
    struct in_addr sin_addr;    /* Internet address.  */
    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
               sizeof (in_port_t) - sizeof (struct in_addr)];
};  
struct in_addr
{
    in_addr_t s_addr;
};
struct sockaddr_in6
{
    sa_family_t sin6_family;
    in_port_t sin6_port;    /* Transport layer port # */
    uint32_t sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id; /* IPv6 scope-id */
 };
typedef unsigned short  uint16_t;
typedef unsigned int    uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)

创建和初始化

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0
saddr.sin_port = htons(9999);

我们一般使用的IP地址表示方法为点分十进制表示法(一个字符串),而sockaddr_in中存储的IP地址为一般为整形的网络字节序形式,因此我们可以使用inet_pton函数来实现点分十进制IP地址字符床与网络字节序整数的转换。

当让我们可以直接给IP地址复制INADDR_ANY或者0.0.0.0二者的值是相同的,其意义是让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求

绑定
我们使用bind函数来实现绑定

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命- 功能:绑定,将fd 和本地的IP + 端口进行绑定
    - 参数:
            - sockfd : 通过socket函数得到的文件描述符
            - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
            - addrlen : 第二个参数结构体占的内存大小
 // 2.绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

tip:注意对&saddr进行强制类型转换(struct sockaddr *)&saddr

早期网络编程函数使用通用socket地址struct sockaddr结构体,为了向前兼容,现在sockaddr退化成了void *,也就是说这片内存区域的使用和解释方法取决于谁去用和怎么用。

专用socket地址类型变量在实际使用时都需要转化为通用socket地址类型socketaddr,因此在具体使用这些接口的时候必须指定具体的指针类型

3.设置监听,监听的fd开始工作

我们使用listen函数来进行监听,listen创建一个监听队列来接收客户端的连接,backlog参数就是设置监听队列的长度一般为5,如果设置为SOMAXCONN那么就由系统来决定,一般是比较大的。
另外,如果监听队列满了那么客户端会收到 ECONNREFUSED 错误

int listen(int sockfd, int backlog);    // /proc/sys/net/core/somaxconn
    - 功能:监听这个socket上的连接
    - 参数:
        - sockfd : 通过socket()函数得到的文件描述符
        - backlog : 未连接的和已经连接的和的最大值, 5
 // 3.监听
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

4.阻塞等待客户端发起链接

我们使用accept函数接收客户端发起的连接,并记录下来
因为需要记录因此,我们还需要一个struct socketaddr来存储客户端的信息

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 
    - 参数:
            - sockfd : 用于监听的文件描述符
            - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
            - addrlen : 指定第二个参数的对应的内存大小
    - 返回值:
            - 成功 :用于通信的文件描述符 
            - -1 : 失败
                
 struct sockaddr_in clientaddr;
 int len = sizeof(clientaddr);
 int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
 
 if(cfd == -1) {
     perror("accept");
     exit(-1);
 }

如果我们要查看客户端信息,那么最好使用inet_ntop将存储的网络字节序转化为点分十进制表示法,用ntohs将端口从网络字节序转化为主机字节序

    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\\n", clientIP, clientPort);

5.通信收发数据

还是那句话,Linux系统中万物皆是文件,socket也不例外。
在文件描述符后就可以使用readwrite直接进行读写操作了

// 5.通信
    char recvBuf[1024] = {0};
    while(1) {
        
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
            perror("read");
            exit(-1);
        } else if(num > 0) {
            printf("recv client data : %s\\n", recvBuf);
        } else if(num == 0) {
            // 表示客户端断开连接
            printf("clinet closed...");
            break;
        }

        char * data = "hello,i am server";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
    }

6.通信结束断开链接

当通信结束断开连接的时候,我们需要关闭监听的文件描述符和客户端的文件描述符

    // 关闭文件描述符
    close(cfd);
    close(lfd);

最终代码

// TCP 通信的服务器端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
    
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {0};
    while(1) {
        
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
            perror("read");
            exit(-1);
        } else if(num > 0) {
            printf("recv client data : %s\\n", recvBuf);
        } else if(num == 0) {
            // 表示客户端断开连接
            printf("clinet closed...");
            break;
        }

        char * data = "hello,i am server";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
    }
   
    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

TCP通信客户端

客户端比服务端就简单许多了

1.创建一个用于通信的套接字

// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
    perror("socket");
    exit(-1);
}

2.连接服务器,指定连接服务器的IP和端口

// 2.连接服务器端
 struct sockaddr_in serveraddr;
 serveraddr.sin_family = AF_INET;
 inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
 serveraddr.sin_port = htons(9999);
 int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

 if(ret == -1) {
     perror("connect");
     exit(-1);
 }

3.连接成功收发数据

// 3. 通信
char recvBuf[1024] = {0};
while(1) {

    char * data = "hello,i am client";
    // 给客户端发送数据
    write(fd, data , strlen(data));

    sleep(1);
    
    int len = read(fd, recvBuf, sizeof(recvBuf));
    if(len == -1) {
        perror("read");
        exit(-1);
    } else if(len > 0) {
        printf("recv server data : %s\\n", recvBuf);
    } else if(len == 0) {
        // 表示服务器端断开连接
        printf("server closed...");
        break;
    }

}

4.通信结束,断开连接

close(fd);

5.最终代码

// TCP通信的客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    
    // 3. 通信
    char recvBuf[1024] = {0};
    while(1) {

        char * data = "hello,i am client";
        // 给客户端发送数据
        write(fd, data , strlen(data));

        sleep(1);
        
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server data : %s\\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

    }

    // 关闭连接
    close(fd);

    return 0;
}

运行结果

通过ifconfig查看ip地址,做适当修改,在本机进行socket测试

以上是关于[Linux 高并发服务器]TCP通信流程的主要内容,如果未能解决你的问题,请参考以下文章

Linux下解决高并发socket最大连接数限制,tcp默认1024个连接

Linux下解决高并发socket最大连接数限制,tcp默认1024个连接

[Linux高并发服务器]进程间通信简介

C/C++ 服务器并发

Linux中,Tomcat 怎么承载高并发(深入Tcp参数 backlog)

C/C++ 服务器并发