当前位置: 首页 > article >正文

Linux -- 初步了解 TCP 编程

目录

TCP 协议

 TCP 编程流程图

listen 函数(监听连接请求)

accept 函数(等待并接收连接)

connect 函数(建立连接)

shutdown 函数

gitee

主要代码

TcpServer.hpp

如何让服务器一次处理多个客户端的请求?

version 多进程:

version 多线程:

version 线程池:

完整代码: 

MainClient.cc

运行结果

version 多进程: 

version 多线程:

version 线程池: 


TCP 协议

TCP(传输控制协议,Transmission Control Protocol)是互联网协议套件中的核心协议之一,它提供了面向连接、可靠字节流服务。

因为 TCP 是面向连接的协议,在客户端和服务器通信前,需要先建立连接

TCP 编程流程图

listen 函数(监听连接请求)

#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd 是想要设置为监听模式的套接字描述符。这个描述符是由之前的 socket() 系统调用返回的。

backlog 参数指定了操作系统可以为该套接字排队的最大连接请求数。它是一个建议性的最大值,实际的最大长度可能会由操作系统限定。

  • listen() 成功执行时,它会返回 0
  • 如果发生错误,则返回 -1,并且会设置全局变量 errno 来指示具体的错误类型。

listen 被调用后,套接字就进入了监听状态,并开始排队连接请求。一旦有新的连接请求到达,它会被加入到队列中,直到服务器程序调用 accept 函数来处理这些连接请求。 

accept 函数(等待并接收连接)

#include <sys/types.h>
#include <sys/socket.h>

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

sockfd:这是由之前的 socket() 系统调用创建,并通过 bind() 和 listen() 设置为监听模式的套接字描述符。

addr:这是一个指向 struct sockaddr 结构的指针,用于返回已连接客户端的地址信息。如果不需要客户端地址信息,可以将此参数设置为 NULL。是输出型参数。

addrlen:这是一个指向 socklen_t 类型变量的指针,该变量在调用时应包含 addr 指向结构的大小(以字节为单位)。调用后,它会被更新为实际存储在 addr 中的地址长度。如果 addr 是 NULL,那么这个参数也可以是 NULL。是输出型参数

返回值:

  • 如果成功accept 返回一个新的文件描述符,这个描述符代表与客户端之间的连接。服务器程序可以通过这个新描述符与客户端通信。
  • 如果有错误发生,accept 返回 -1,并设置全局变量 errno 以指示错误类型。

accept 函数会阻塞直到有一个新的连接建立。当有连接到达并且操作系统将其加入到队列中之后,accept 就会返回一个新连接的文件描述符,而原来的监听套接字 sockfd 仍然保持监听状态,可以继续接收更多的连接请求。

如何理解监听套接字 sockfd 和 accept 的返回值之间的关系呢?

一般的餐饮店门口会有一个揽客的,店内也会有服务员,揽客的揽到客人之后,由服务员为客人提供服务,揽客的继续在店门口揽客,并不会做服务员的工作,服务员也只做服务员的工作,不会到门口揽客,两个人各司其职。揽客的就是监听套接字 sockfd,服务员就是 accept 的返回值,sockfd 接收到连接后,就继续监听,accept 的返回值会为通信提供服务。

connect 函数(建立连接)

connect 函数是用于建立客户端与服务器之间的连接的系统调用或库函数。它主要用于 TCP(传输控制协议)套接字,但也可以用于其他类型的面向连接的协议。当一个程序需要主动发起一个网络连接到远程服务时,就会使用 connect 函数

#include <sys/types.h>
#include <sys/socket.h>

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

sockfd 是一个整数,表示之前通过 socket() 系统调用创建的未连接套接字描述符。

addr 是一个指向 sockaddr 结构的指针,该结构包含了要连接的服务器地址信息。通常你会使用 sockaddr_in 或 sockaddr_in6 来具体指定 IPv4 或 IPv6 地址及端口。

addrlen 是上述地址结构的大小,以字节为单位。

返回值:

  • 如果成功connect 返回 0
  • 如果发生错误,则返回 -1,并设置 errno 变量以指示错误类型。

shutdown 函数(关闭套接字描述符)

用完的套接字描述符必须关掉,因为描述符的数量是有限的,用完不关掉会导致描述符泄露

#include <sys/socket.h>

int shutdown(int sockfd, int how);

 sockfd 是一个整数,表示要关闭的套接字描述符。

how 是一个整数,指定了如何关闭套接字,它可以取以下值之一:

  • SHUT_RD不再接收数据。对于双向套接字,这会阻止进一步的数据接收。
  • SHUT_WR不再发送数据。对于双向套接字,这会发送一个FIN包给对端,表明发送方已经完成发送,并且不会再发送更多数据。
  • SHUT_RDWR不再接收不再发送数据。这是组合了前两种情况的效果,等价于分别调用 SHUT_RD 和 SHUT_WR

返回值:

  • 如果成功shutdown 返回 0
  • 如果发生错误,则返回 -1,并设置 errno 变量以指示错误类型。

gitee

tcp_echo_server · zihuixie/Linux_Learning - 码云 - 开源中国icon-default.png?t=O83Ahttps://gitee.com/zihuixie/linux_-learning/tree/master/tcp_echo_server

主要代码

TcpServer.hpp

如何让服务器一次处理多个客户端的请求?

version 多进程:

为什么创建子进程?

创建子进程后,父进程继续接收连接,子进程则处理通信任务,提供服务,就可以实现一次处理多个请求。

为什么关掉描述符?

创建子进程时,父进程就把 accept 的返回值 sockfd 交给了子进程。

父进程如果不关掉 sockfd,会导致可用的 套接字描述符 越来越少,而且父进程不需要用到 sockfd,父进程只需要监听。子进程也不需要监听,所以子进程关掉 _listensock。

由于进程的独立性,子进程 关掉 _listensock 并不会影响 父进程继续监听,父进程关掉 sockfd 并不会影响子进程处理通信任务。

为什么创建孙子进程?

如果父进程等待子进程的执行,那么父进程需要等待 子进程 处理完当前的任务才可以继续接收连接,那么服务器还是一次只能处理一个请求,所以需要分离父子进程。

创建孙子进程,就可以解决服务器一次处理多个请求!

创建孙子进程后,子进程退出,那么孙子进程变成僵尸进程,由系统收养,父进程和子进程都不需要关心孙子进程的执行,父进程可以继续接收连接。

//创建子进程
pid_t id = fork();
if (id == 0) // 子进程
{
    close(_listensock);//子进程不需要监听

    //创建孙子进程
    if (fork() > 0)
        exit(0); // 子进程退出

    //孙子进程变僵尸进程,由系统收养
    Service(sockfd, InetAddr(peer)); // 孙子进程执行任务
    exit(0);
}

// 父进程
close(sockfd);//父进程不需要提供服务
waitpid(id,nullptr,0);
version 多线程:

注意线程不需要关掉描述符,因为线程间共享描述符表

pthread_t t;
ThreadData *data =new ThreadData(sockfd,InetAddr(peer),this);

pthread_create(&t,nullptr,HandlerSock,data);
version 线程池:

多线程版本中是客户端请求连接后再创建线程,线程池是先创建线程,客户端请求连接时,服务器可以直接使用线程处理请求。

task_t t=std::bind(&TcpServer::Service,this,sockfd,InetAddr(peer));
ThreadPool<task_t>::GetInstance()->Enqueue(t);

完整代码: 

#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"

const static int gbacklog = 16;
const static int defaultsockfd = -1;
using task_t = std::function<void()>;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR

};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd,InetAddr addr,TcpServer *s)
    :_sockfd(fd),_ClientAddr(addr),_self(s)
    {}
public:
    int _sockfd;
    InetAddr _ClientAddr;
    TcpServer *_self;
};

class TcpServer
{
public:
    TcpServer(uint16_t port)
        : _port(port), _isrunning(false), _listensock(defaultsockfd)
    {
    }
    ~TcpServer()
    {
        if (_listensock > defaultsockfd)
        {
            close(_listensock); // 关闭监听
        }
    }

    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // TCP协议用SOCK_STREAM

        // 创建失败
        if (_listensock < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, sockfd: %d\n", _listensock);

        // 填地址
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);

        // 绑定
        int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local));

        // 绑定失败
        if (n < 0)
        {
            LOG(FATAL, "%d bind error\n", _port);
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "bind success,sockfd: %d\n", _listensock);

        // 开始监听连接
        n = listen(_listensock, gbacklog);

        // 监听失败
        if (n < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG, "listen success,sockfd: %d\n", _listensock);
    }

    void Service(int sockfd, InetAddr client)
    {
        LOG(DEBUG, "get a new link, info %s:%d, fd: %d \n", client.Ip().c_str(), client.Port(), sockfd);

        // 发送方端口号
        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]#";

        // 缓冲区
        char inbuffer[1024];

        while (true)
        {
            // 读取字节流
            //  n:读到的字节数
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);

            if (n > 0)
            {
                inbuffer[n] = 0;

                std::cout << clientaddr << inbuffer << std::endl;

                // 应答
                std::string echo_string = "[server echo]#";
                echo_string += inbuffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0) // 读到文件结尾
            {
                // 尝试从当前位置继续读取数据但没有更多的数据可以读取
                // 那么就说程序已经到达了文件的结尾,client 退出
                LOG(INFO, "%s quit\n", clientaddr.c_str());
                break;
            }
            else // 读取失败
            {
                LOG(ERROR, "read error\n");
                break;
            }
        }

        // 服务器开始退出
        std::cout << "server start to quit..." << std::endl;

        // sockfd 才是提供服务的套接字描述符,_listensock 用于监听
        // 需要关掉提供提供服务的 sockfd
        shutdown(sockfd, SHUT_RD);

        std::cout << "shut_rd" << std::endl;
    }

    static void* HandlerSock(void *args)
    {
        pthread_detach(pthread_self());//分离新、主线程

        ThreadData *td=static_cast<ThreadData*>(args);

        td->_self->Service(td->_sockfd,td->_ClientAddr);

        delete td;

        return nullptr;
    }

    // 循环接收连接
    void Loop()
    {
        _isrunning = true;

        while (_isrunning)
        {
            // 输出型参数
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            // 等待并接收连接
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);

            // 接收失败
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");

                // 本次接收失败之后还可以继续接收
                continue;
            }

            // 开始执行任务

            // version 0: 一次只能处理一个请求
            // Service(sockfd,InetAddr(peer));

            // version 1:用多进程

            // // 创建子进程
            // pid_t id = fork();
            // if (id == 0) // 子进程
            // {
            //     close(_listensock);//子进程不需要监听

            //     // 创建孙子进程
            //     if (fork() > 0)
            //         exit(0); // 子进程退出

            //     //孙子进程变僵尸进程,由系统收养
            //     Service(sockfd, InetAddr(peer)); // 孙子进程执行任务
            //     exit(0);
            // }

            // // 父进程
            // close(sockfd);//父进程不需要提供服务
            // waitpid(id,nullptr,0);

            // // version 2:用多线程
            // pthread_t t;
            // ThreadData *data =new ThreadData(sockfd,InetAddr(peer),this);

            // pthread_create(&t,nullptr,HandlerSock,data);

            // version 3:用线程池

            task_t t=std::bind(&TcpServer::Service,this,sockfd,InetAddr(peer));
            ThreadPool<task_t>::GetInstance()->Enqueue(t);
        }

        _isrunning = false;
    }

private:
    int _listensock; // 用于监听
    uint16_t _port;
    bool _isrunning; // 是否正在运行
};

MainClient.cc

#include <iostream>
#include <sys/types.h>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << "serverip serverport" << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    uint16_t serverport = std::stoi(argv[2]);
    std::string serverip = argv[1];

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    struct sockaddr_in server;
    memset(&server, 0, sizeof(struct sockaddr_in));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    // 客户端不需要调用bind 函数

    // 客户端与服务器建立连接
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));

    // 连接建立失败
    if (n < 0)
    {
        std::cerr<<"connect error"<<std::endl;
        exit(3);
    }

    while(true)
    {
        //1、输入消息
        std::cout<<"\nPlease Enter# ";

        std::string message;
        std::getline(std::cin,message);

        //2、发送消息
        ssize_t s=send(sockfd,message.c_str(),message.size(),0);

        if(s>0)
        {
            //3、接收应答
            char inbuffer[1024];
            ssize_t r=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);

            //接收成功
            if(r>0)
            {
                inbuffer[r]=0;
                std::cout<<inbuffer<<std::endl;
            }
            //接收失败
            else
            {
                break;
            }

        }
        else
        {
            break;
        }
        
    }

    shutdown(sockfd,SHUT_WR);

    return 0;
}

运行结果

不同的端口号 可以看出服务器可以一次处理多个请求!

version 多进程: 

 

 

 

version 多线程:

 

version 线程池: 

 

 


http://www.kler.cn/a/488298.html

相关文章:

  • 30天开发操作系统 第 12 天 -- 定时器 v1.0
  • Idea-离线安装SonarLint插件地址
  • Selenium 的四种等待方式及使用场景
  • mysql中查询json的技巧
  • 【gRPC】Keepalive连接保活配置,go案例
  • 2025新春烟花代码(二)HTML5实现孔明灯和烟花效果
  • ubuntu 20.04 安装 5.4 内核
  • CClinkIEfield Basic转Modbus TCP网关模块连接三菱FX5U PLC
  • 关于物联网的基础知识(四)——国内有代表性的物联网平台都有哪些?
  • xml-dota-yolo数据集格式转换
  • 【FPGA】时序约束与分析
  • 部署langchain服务
  • 使用 FastAPI 和 Async Python 打造高性能 API
  • 超大规模分类(三):KNN softmax
  • cJson——序列化格式json和protobuf对比
  • 单元测试MockitoExtension和SpringExtension
  • poi处理多选框进行勾选操作下载word以及多word文件压缩
  • Cognitive architecture 又是个什么东东?
  • 【Rust自学】11.7. 按测试的名称运行测试
  • 记录一个移动端表格布局,就是一行标题,下面一列是对应的数据,一条一条的数据,还有点击数据进入详情的图标,还可以给一列加input输入框,还可以一对多
  • dubbo3 负载均衡
  • js迭代器模式
  • python+camelot库:提取pdf中的表格数据
  • 工厂人员定位管理系统方案(二)人员精确定位系统架构设计,适用于工厂智能管理
  • 《零基础Go语言算法实战》【题目 2-1】使用一个函数比较两个整数
  • iOS - 数组的真实类型