Linux 基于TCP实现服务端客户端通信(多进程/多线程版)

在单进程/单线程的版本中,一个用户连接上来了之后,与服务端进行通信,其他的客户端是无法与服务端通信的,StartServer() 里调用 accept() 获取新连接后,立刻进入 Service() 循环。Service() 是死循环,会一直阻塞在 read() ,等待当前客户端发数据。只要这个客户端不断开,函数就不会返回, accept() 永远无法再次执行,新连接只能排队,无法处理。

所以第一个想法使用多进程可不可以解决呢?

主线程原来接听到来的客户端,到有客户端到来时,创建一个子进程来执行对连接上来的客户端的操作。

子进程做什么?

  1. fork 返回值 == 0,进入子进程

  2. 子进程不关心监听套接字的事情,close(listensock_),

  3. 和客户端通信(Service)

  4. 通信结束 close(sockfd)

  5. exit() 退出子进程

父进程做什么?

  1. fork 返回值 != 0,父进程

  2. sockfd已经交给子进程去管理了,父进程不关系了,close(sockfd)

  3. waitpid() 等待子进程结束

  4. 继续循环,accept 新连接

cpp 复制代码
void StartServer()
{
    lg(Info, "tcpserver is running...");

    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        
        int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
            continue;
        }
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

        lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
            sockfd, clientip, clientport);
        //version 1 单进程/单线程版
        // Service(sockfd, clientip, clientport);
        // close(sockfd);

        //version 2 多进程版
        pid_t id = fork();
        if(id == 0)
        {
            //child
            close(listensock_);

            Service(sockfd, clientip, clientport);
            close(sockfd);
            
            exit(0);
        }
        close(sockfd);
        pid_t rid = waitpid(id, nullptr, 0);
        if(rid == id)
            std::cout << "father wait success" << std::endl;
    }
}

这个想法看起来可以,但是是不行的,在父进程监听到一个连接后,交给了子进程,但是子进程对于分发的客户端的服务是长时间,也就是说子进程不会退出,我们的父进程会阻塞的在哪里等待着子进程,也就无法监听到新的连接了。

使用主孙进程来实现进程的并行

原本的多进程:父进程 fork创建子进程,子进程对连接的客户端进行通信,父进程 waitpid阻塞子进程。

问题:waitpid 会阻塞父进程,直到子进程退出,父进程才能继续 accept。

不能让父进程等子进程!要让父进程立刻继续监听,虽然父进程会阻塞等待子进程的退出,但是不会等待孙进程的退出。父进程(服务器主进程)accept 到新连接,父进程 fork 出子进程,子进程立刻再 fork 出孙子进程,由孙子进程来负责和客户端通信(执行 Service),子进程:直接 exit 退出,父进程:很快 wait 到子进程退出,不阻塞,继续 accept。

子进程退出后,孙子进程没有父进程了,变成孤儿进程。操作系统会自动把孤儿进程领养给 init/systemd。

代码:

cpp 复制代码
void StartServer()
    {
        lg(Info,"TcpServer is running");
        for(;;)
        {
            struct sockaddr_in client;
            socklen_t len=sizeof(client);
            //阻塞等待
            int sockfd=accept(listensock_,(struct sockaddr*)(&client),&len);
            if(sockfd<0)
            {
                lg(Warning,"accept error,errno:%d,errstring:%s",errno,strerror(errno));
                continue;
            }
            uint16_t clientport=ntohs(client.sin_port);
            string clientip=inet_ntoa(client.sin_addr);
            lg(Info,"get a new link...,clientip:%s,clientport:%d",clientip,clientport);
            pid_t id=fork();
            if(id==0)
            {
                //子进程
                close(listensock_);
                //子进程创建孙进程后直接退出
                if(fork()>0) exit(0);
                //孙进程
                Service(sockfd,clientip,clientport);
                close(sockfd);
            }
            close(sockfd);
            pid_t rid=waitpid(id,nullptr,0);
        }
    }

代码测试:

运行结果:

即使在有多个客户端连接上来之后,服务端还是可以监听到,创建孙进程来完成进程间的并行。

多线程版

将需要传递给线程函数的变量使用一个类进行封装,之后只需要传类指针即可。类里面的参数包括服务器类的 this 指针,客户端 sockfd ,客户端 IP,客户端 port

cpp 复制代码
class TcpServer;
struct ThreadData
{
public:
    ThreadData(int fd,const string&ip,const uint16_t&p,TcpServer*t)
    :sockfd(fd)
    ,clientip(ip)
    ,clientport(p)
    ,tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer*tsvr;
};

线程传递函数

线程函数 Routine 负责调用 Service 和客户端通信,使用传递进来的ThreadData类中的TcpServer指针来进行调用Service与客户端通信。

注意:线程函数必须是 static

类成员函数默认带 this ,参数格式和 pthread_create 要求不匹配,加 static 后,函数没有 this ,类型匹配,可直接传给线程库。

cpp 复制代码
static void*Routine(void*args)
    {
        pthread_detach(pthread_self());
        ThreadData*td=static_cast<ThreadData*>(args);
        td->tsvr->Service(td->sockfd,td->clientip,td->clientport);
        close(td->sockfd);
        delete td;
    }

创建线程执行线程函数

为监听到的连接创建线程,执行线程函数。

cpp 复制代码
void StartServer()
    {
        lg(Info,"TcpServer is running");
        for(;;)
        {
            struct sockaddr_in client;
            socklen_t len=sizeof(client);
            //阻塞等待
            int sockfd=accept(listensock_,(struct sockaddr*)(&client),&len);
            if(sockfd<0)
            {
                lg(Warning,"accept error,errno:%d,errstring:%s",errno,strerror(errno));
                continue;
            }
            uint16_t clientport=ntohs(client.sin_port);
            string clientip=inet_ntoa(client.sin_addr);
            lg(Info,"get a new link...,clientip:%s,clientport:%d",clientip,clientport);
            // pid_t id=fork();
            // if(id==0)
            // {
            //     //子进程
            //     close(listensock_);
            //     //子进程创建孙进程后直接退出
            //     if(fork()>0) exit(0);
            //     //孙进程
            //     Service(sockfd,clientip,clientport);
            //     close(sockfd);
            // }
            // close(sockfd);
            // pid_t rid=waitpid(id,nullptr,0);
            ThreadData*td=new ThreadData(sockfd,clientip,clientport,this);
            pthread_t id;
            pthread_create(&id,nullptr,Routine,td);
        }
    }

代码测试:

源代码:

client.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(const string& str)
{
    cout << "\n\tUsage: " << str << " serverip serverport" << endl; 
}

// 客户端启动格式:./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

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

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        cerr << "socket create err" << endl;
        return 1;
    }
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);
    server.sin_addr.s_addr=inet_addr(serverip.c_str());
    socklen_t len=sizeof(server);
    //客户端发起connect请求时,操作系统会自动进行端口号的随机bind绑定
    int n=connect(sockfd,(struct sockaddr*)(&server),len);
    if(n<0)
    {
        perror("connect error");
        cerr<<"connect error..."<<endl;
        return 2;
    }
    string message;
    char inbuffer[4096];
    while(true)
    {
        cout << "Please Enter# ";
        getline(cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
        {
            cerr << "write err" << endl;
            break;
        }

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
        else
        {
            break;
        }
    }
    close(sockfd);
    return 0;
}

main.cpp

cpp 复制代码
#include <iostream>
#include <memory>
#include "server.hpp"
void Usage(const std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> server(new TcpServer(port));
    server->init();
    server->StartServer();
    return 0;
}

makefile

cpp 复制代码
.PHONY:all
all:server client
server:main.cpp
	g++ -o $@ $^ -std=c++11 -lpthread
client:client.cpp
	g++ -o $@ $^ -std=c++11 
.PHONY:clean
clean:
	rm -f server client

server.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "log.hpp"
extern Log lg;
using namespace std;
const int backlog=10;
const string ip="0.0.0.0";
const uint16_t port=8080;
enum
{
    UsageError=1,
    SocketError,
    BindError,
    ListenError
};
class TcpServer;
struct ThreadData
{
public:
    ThreadData(int fd,const string&ip,const uint16_t&p,TcpServer*t)
    :sockfd(fd)
    ,clientip(ip)
    ,clientport(p)
    ,tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer*tsvr;
};
class TcpServer
{
public:
    TcpServer(uint16_t defaultport=port)
    :ip_(ip)
    ,port_(port)
    {}
    void init()
    {
        listensock_=socket(AF_INET,SOCK_STREAM,0);
        if(listensock_<0)
        {
            lg(Fatal,"create socket error,errno:%d,errstring:%s",errno,strerror(errno));
            exit(SocketError);
        }
        lg(Info,"socked create success,listensock_:%d",listensock_);
        //防止偶发性服务器无法重启
        int opt=1;
        setsockopt(listensock_,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
        struct sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family=AF_INET;
        server.sin_port=htons(port_);
        server.sin_addr.s_addr=inet_addr(ip_.c_str());
        socklen_t len=sizeof(server);
        if(bind(listensock_,(struct sockaddr*)(&server),len)<0)
        {
            lg(Fatal,"bind socket error,errno:%d,errstring:%s",errno,strerror(errno));
            exit(BindError);
        }
        lg(Info,"socked bind success");
        if(listen(listensock_,backlog)<0)
        {
            lg(Fatal,"listen error,errno:%d,errstring:%s",errno,strerror(errno));
            exit(ListenError);
        }
        lg(Info,"listen success");
    }
    void Service(int sockfd,string clientip,uint16_t clientport)
    {
        while(true)
        {
            char buffer[4096];
            ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);
            if(n>0)
            {
                buffer[n]=0;
                cout<<"client say#"<<buffer<<endl;
                string server_echo="server say#";
                server_echo+=buffer;
                write(sockfd,server_echo.c_str(),server_echo.size());
            }
            else if(n==0)
            {
                //客户端断开连接
                lg(Info,"%s:%d quit ,server close sockfd:%d",clientip,clientport,sockfd);
                break;
            }
            else
            {
                //异常情况
                lg(Info,"read error,%s:%d quit,server close sockfd:%d",clientip,clientport,sockfd);
                break;
            }
        }
    }
    static void*Routine(void*args)
    {
        pthread_detach(pthread_self());
        ThreadData*td=static_cast<ThreadData*>(args);
        td->tsvr->Service(td->sockfd,td->clientip,td->clientport);
        close(td->sockfd);
        delete td;
    }
    void StartServer()
    {
        lg(Info,"TcpServer is running");
        for(;;)
        {
            struct sockaddr_in client;
            socklen_t len=sizeof(client);
            //阻塞等待
            int sockfd=accept(listensock_,(struct sockaddr*)(&client),&len);
            if(sockfd<0)
            {
                lg(Warning,"accept error,errno:%d,errstring:%s",errno,strerror(errno));
                continue;
            }
            uint16_t clientport=ntohs(client.sin_port);
            string clientip=inet_ntoa(client.sin_addr);
            lg(Info,"get a new link...,clientip:%s,clientport:%d",clientip,clientport);
            // pid_t id=fork();
            // if(id==0)
            // {
            //     //子进程
            //     close(listensock_);
            //     //子进程创建孙进程后直接退出
            //     if(fork()>0) exit(0);
            //     //孙进程
            //     Service(sockfd,clientip,clientport);
            //     close(sockfd);
            // }
            // close(sockfd);
            // pid_t rid=waitpid(id,nullptr,0);
            ThreadData*td=new ThreadData(sockfd,clientip,clientport,this);
            pthread_t id;
            pthread_create(&id,nullptr,Routine,td);
        }
    }
private:
    int listensock_;
    string ip_;
    uint16_t port_;
};
相关推荐
星辰_mya2 小时前
CompletableFuture:异步编程的“智能机械臂”
java·开发语言·面试
一见2 小时前
WorkBuddy安装Skill的方法
android·java·javascript
M158227690552 小时前
SG-TCP-COE-210 Modbus TCP 转 CANOpen 网关:跨协议工业通信的无缝互联方案
网络·网络协议·tcp/ip
报错小能手2 小时前
nginx集群聊天室(五)nginx配置tcp服务器负载均衡
服务器·tcp/ip·nginx
悟空码字2 小时前
SpringBoot + 腾讯地图实战:打造全能型地理位置服务平台,开箱即用!
java·spring boot·后端
小光学长2 小时前
基于ssm的书法学习交流系统25ki07v1(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·开发语言·数据库·学习·ssm
报错小能手2 小时前
如何手撕集群聊天室项目?
linux·服务器
金智维科技官方2 小时前
Agent架构综述:从Prompt到Context
java·微服务·架构·agent
@小明月2 小时前
前端进阶之路
java·前端·笔记