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

Linux网络编程——简单的TCP网络通信

目录

前言

TCP套接字接口

简单的TCP网络通信

.hpp文件

 打印日志文件 logMessage.hpp

TCP服务端实现

1、listen()

2、 accept() 

完整代码 

TCP客户端的实现

connect()接口 

完整代码 

多进程版本一

多进程版本二


前言

上篇文章中我们对udp网络通信有了初步的认识,我们在之前已经知道了UDP通信和TCP通信的不同

  • UDP非连接,面向数据包。
  • TCP连接,面向字节流。

这篇文章我们来介绍和演示一下TCP套接字的接口。

TCP套接字接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

// 发送报文 (UDP)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

// 接收报文 (UDP)
ssize_t recvfrom(int socket, void* restrict buffer, size_t length, int flags, struct sockaddr* restrict address, socklen_t* restrict address_len);

上篇文中我们已经演示过了里面的一些接口,剩下的三个用于连接的接口就在下面演示一遍。

简单的TCP网络通信

在学习过UDP之后,TCP这里出了新的接口之外,实际上并没有多少新的东西,无非就是连接的细节不一样罢了,下面直接演示:

.hpp文件

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "logMessage.hpp"

#define SOCKET_ERR  1
#define BIND_ERR    2
#define LISTEN_ERR  3
#define USE_ERR     4
#define CONNECT_ERR	5
#define FORK_ERR	6

#define BUFFER_SIZE	1024

 打印日志文件 logMessage.hpp

与UDP的打印文件相同,不做过多解释

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <cstdlib>

// 宏定义 四个日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

// 实现一个 可以输出: 日志等级、日志时间、用户、以及相关日志内容的 日志消息打印接口
void logMessage(int level, const char* format, ...) {
    // 通过可变参数实现, 传入日志等级, 日志内容格式, 日志内容相关参数

    // 确保日志等级正确
    assert(level >= DEBUG);
    assert(level <= FATAL);

    // 获取当前用户名
    char* name = getenv("USER");

    // 简单的定义log缓冲区
    char logInfo[1024];

    // 定义一个指向可变参数列表的指针
    va_list ap;
    // 将 ap 指向可变参数列表中的第一个参数, 即 format 之后的第一个参数
    va_start(ap, format);

    // 此函数 会通过 ap 遍历可变参数列表, 然后根据 format 字符串指定的格式, 将ap当前指向的参数以字符串的形式 写入到logInfo缓冲区中
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    // ap 使用完之后, 再将 ap置空
    va_end(ap); // ap = NULL

    // 通过判断日志等级, 来选择是标准输出流还是标准错误流
    FILE* out = (level == FATAL) ? stderr : stdout;

    // 获取本地时间
    time_t tm = time(nullptr);
    struct tm* localTm = localtime(&tm);
    char* localTmStr = asctime(localTm);
    char* nC = strstr(localTmStr, "\n");
    if(nC) {
        *nC = '\0';
    }
    fprintf( out, "%s | %s | %s | %s\n", 
            log_level[level],
            localTmStr,
            name == nullptr ? "unknow" : name, 
            logInfo );
}

TCP服务端实现

我们还是由浅入深,实现一个简单点的TCP服务器,服务器包含一个对客户端发送来的消息进行小写转大写的功能。要实现TCP网络通信服务端都需要哪些步骤,与UDP的区别具体是什么呢?

  1. 创建套接字:使用 socket() 函数创建一个新的套接字。
  2. 绑定地址和端口:通过 bind() 将该套接字绑定到一个特定的IP地址和端口号上。
  3. 监听连接请求:调用 listen() 函数让套接字进入监听状态,准备接受连接请求。
  4. 接受连接:使用 accept() 函数接受来自客户端的连接请求。这将返回一个新的套接字描述符,用于与特定客户端进行通信。
  5. 数据交换:使用 send() 和 recv() (或 write() 和 read())函数来发送和接收数据。
  6. 关闭连接:通信完成后,使用 close() 关闭连接。

可以看到TCP通信服务端与UDP通信服务端的主要差别在于监听连接请求、接收连接这两步。

先来分别介绍一下这几个新的接口

1、listen()

int listen(int sockfd, int backlog);

与UDP不同,TCP网络通信是双方要以建立连接为基础的。在TCP通信中,listen 是服务器端用于开始监听传入连接请求的一个重要函数。它是网络编程中实现服务端逻辑的关键步骤之一。


接口参数:

  •  sockfd: 这是一个整数类型的文件描述符,指向之前通过调用 socket() 创建的套接字。这个套接字需要绑定到一个本地地址(使用 bind() 函数)。
  • backlog: 它指定了待处理连接请求队列的最大长度。也就是说,它决定了系统可以为该套接字排队等待接受的最大未处理连接数。如果连接请求数超过了这个值,新的请求可能会被拒绝。(类似餐馆就像是服务器,而座位和等候区加起来就像是 backlog 参数。)

返回值:

  • 成功时,listen() 返回 0。
  • 错误时,返回 -1,并设置全局变量 errno 来指示错误类型。

执行listen(), 服务器会自动进入监听状态. 之后会一直监听 来自客户端 向服务器 发送的连接请求. 实际上监听的就是服务器的套接字. 监听的过程是非阻塞的.

2、 accept() 

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

TCP是 面向连接 的. 其他客户端想要将信息发送到服务器, 就必须先于服务器建立连接.

而能否与服务器建立连接 是服务器说了算的. 只有 服务器接受了来自客户端的连接请求 才是连接成功 了。


接口参数:

  • sockfd: 这是一个整数类型的文件描述符,指向之前通过调用 socket() 创建并且已经通过 bind() listen() 设置为监听状态的套接字。
  • addr: 这是一个指向 struct sockaddr 类型对象的指针,用于存储发起连接的客户端地址信息(如果不需要获取客户端地址信息,可以传递 NULL)。
  • addrlen: 这是一个指向 socklen_t 类型变量的指针,指示 addr 参数指向的对象的大小。在调用 accept 函数前,应设置为该结构体的大小;函数返回时,会更新为实际写入的地址长度。

返回值:

  • 成功时,accept() 返回一个新的套接字描述符,用于和客户端进行通信。
  • 错误时,返回 -1,并设置全局变量 errno 来指示错误类型。

accept 是在服务器端用于接受来自客户端的连接请求的一个重要函数。它通常与 listen 函数配合使用,用来从已完成连接队列中提取第一个连接请求,创建一个新的套接字,并返回引用这个套接字的文件描述符,这个新的套接字不会被监听,服务器原来的套接字,不会被此次调用影响。

也就是说,这个TCP服务器会创建很多个套接字,而UDP只会创建一个属于服务器的套接字。这个新创建的套接字和原本的套接字有什么关系呢?

       服务器原本的套接字已经被listen接口作用,处于监听状态,是为了能够接收到连接信息。当accept接受了客户端的连接请求时,会创建一个存储有 服务器本地网络信息 以及 客户端网络信息的新的套接字,服务器可以通过此套接字与客户端进行通信。

      所以原来的服务器创建的套接字 只用来与客户端建立连接,所以被称为 监听套接字

      创建的新的套接字存储有 服务器本地网络信息 以及 客户端网络信息,并且 此套接字也会被绑定到系统内核中,并于服务器原套接字相独立。此套接字主要是用来给客户端提供服务的套接字

完整代码 

//tcpServer.cc
#include"tcp.hpp"

using std::string;
using std::cerr;
using std::endl;
class tcpServer
{
    public:
        tcpServer(uint16_t port,const string& ip="")//按引用传递提高效率
            :_port(port)
            ,_ip(ip){}
        ~tcpServer()
        {}
        void Init()
        {//面向字节流参数选择SOCK_STREAM
            _LiSockFd=socket(AF_INET,SOCK_STREAM,0);//创建套接字(先创建一个listen的套接字)
            if(_LiSockFd<0)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(SOCKET_ERR);
            }
            logMessage(DEBUG,"socket() success::%d",_LiSockFd);
            struct sockaddr_in local;
            memset(&local,0,sizeof(local));//初始化结构体
            local.sin_family=AF_INET;//填充
            local.sin_port=htons(_port);
            _ip.empty()?(local.sin_addr.s_addr=htonl(INADDR_ANY)):(inet_aton(_ip.c_str(),&local.sin_addr));
            
            if(bind(_LiSockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(BIND_ERR);
            }
            logMessage(DEBUG,"bind() success::%d",_LiSockFd);

            if(listen(_LiSockFd,5)==-1)//执行listen(), 服务器会自动进入监听状态. 之后会一直监听 来自客户端 向服务器 发送的连接请求. 实际上监听的就是服务器的套接字. 监听的过程是非阻塞的.
            {
                logMessage(FATAL,"listen() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(LISTEN_ERR);
            }
            logMessage(DEBUG,"listen() success::%d",_LiSockFd);
        }
        void to_upper(int sock,const string& clientIP,const uint16_t &clientPort)//小写转大写功能函数
        {
            assert(sock>0);
            assert(!clientIP.empty());
            
            char inbuffer[BUFFER_SIZE];//用于存储来自客户端信息的数组
            while(true)
            {
                // TCP获取来自客户端的信息的操作就是 read
                // 从 服务器与客户端连接 的文件描述符中 读取来自客户端的信息
                // 可看作 通过文件描述符 从文件读取内容
                ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1);
                if(s>0)
                {
                    inbuffer[s]='\0';
                     // 我们实现一个操作, 如果 客户端传输过来的信息是 quit 这个单词, 就表示客户端请求退出
                    // 就可以退出 服务循环了
                    if(strcasecmp(inbuffer,"quit")==0)
                    {
                        logMessage(DEBUG,"Client requests to quit: [%s: %d]",clientIP.c_str(),clientPort);
                        break;
                    }
                    logMessage(DEBUG,"to_upper before:[%s:%d]>> %s",clientIP.c_str(),clientPort,inbuffer);
                    for(int i=0;i<s;i++)
                    {
                        if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
                            inbuffer[i]=toupper(inbuffer[i]);
                    }
                    logMessage(DEBUG,"to_upper after:[%s:%d]>>%s",clientIP.c_str(),clientPort,inbuffer);
                    // 上面做的都是对获取到的信息 进行转换
                    // 最后需要做的就是 将转换后的信息 再重新回应给客户端
                    // 而 回应给客户端 则是用 write, 可看做 通过文件描述符像文件写入内容
                    write(sock,inbuffer,strlen(inbuffer));
                }
                else if(s==0)
                {
                    logMessage(DEBUG,"Client has quited:[%s:%d],clientIP,clientPort");
                    break;
                }
                else{
                    // 到这里 本次 read() 出错
                    logMessage(DEBUG,"Client [%s:%d] read::%s",clientIP.c_str(),clientPort,strerror(errno));
                    break; 
                }
            }
            // 走到这里 循环已经退出了, 表示 client 也已经退出了
            // 所以 此时需要关闭文件描述符, 因为一个主机上的文件描述符数量是一定的, 达到上限之后 就无法再创建
            // 已经无用但没有被归还的文件描述符, 文件描述符泄漏
            close(sock);
            logMessage(DEBUG,"Service close %d sockFd",sock);
        }
        void start()
        {
            while(true)
            {
                struct sockaddr_in peer;
                socklen_t peerlen=sizeof(peer);
                
                int serviceSockFd=accept(_LiSockFd,(struct sockaddr*)&peer,&peerlen);
                if(serviceSockFd==-1)
                {
                    logMessage(WARINING,"accept() failed::%s : %d",strerror(errno),_LiSockFd);
                    continue;
                }
                string peerIp=inet_ntoa(peer.sin_addr);
                uint16_t peerPort=ntohs(peer.sin_port);
                logMessage(DEBUG,"accept() success:: [%s: %d] | %d",peerIp.c_str(),peerPort,serviceSockFd);
                to_upper(serviceSockFd,peerIp,peerPort);//执行转换功能,小写换大写
            }
        }
    private:
        uint16_t _port;//存储服务器端口号
        string _ip;//存储服务器ip
        int _LiSockFd;//存储服务器进程的监听套接字
};

void Usage(string proc)
{
    cerr<<"Usage::\n\t"<<proc<<"port ip"<<endl;
    cerr<<"example::\n\t"<<proc<<"8080 127.0.0.1"<<endl;
}

int main(int argc,char* argv[])
{
    if(argc!=3&&argc!=2)
    {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }
    tcpServer Usvr(port,ip);
    Usvr.Init();
    Usvr.start();
    return 0;
}

TCP是一种面向流的协议,所以可以直接使用 read() write() 通过文件描述符与客户端进行通信。

TCP客户端的实现

  1. 创建套接字:同样使用 socket() 创建一个新的套接字。
  2. 连接到服务器:使用 connect() 函数尝试连接到指定的服务器地址和端口。
  3. 数据交换:使用 send() 和 recv() 函数发送和接收数据。
  4. 关闭连接:完成通信后,使用 close() 关闭连接。

connect()接口 

在网络编程中,该接口用于客户端连接到服务器。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: 这是一个整数类型的文件描述符,指向之前通过调用 socket() 创建的套接字。
  • addr: 这是一个指向 struct sockaddr 类型对象的指针,包含了你要连接的服务器的地址信息(包括IP地址和端口号)。
  • addrlen: 这是一个 socklen_t 类型的值,表示第二个参数 addr 指向的对象的大小。

同样的,connect() 也会和 sendto()  的那样,自动绑定客户端的网络信息。

等连接成功之后,就可以使用 write() 向服务器发送信息 ,使用 read() 读取服务器的回复了。这个过程中使用的文件描述符都是客户端套接字的文件描述符。

完整代码 

//tcpClient.cc
#include"tcp.hpp"

using std::cerr;
using std::endl;
using std::string;
using std::cin;
using std::getline;
using std::cout;

volatile bool quit=false;
void Usage( string proc)
{
    cerr<<"Usage::\n\t"<<proc<<"serverIp serverPort"<<endl;
    cerr<<"example::\n\t"<<proc<<"127.0.0.1 8080"<<endl;
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    string serverIp=argv[1];
    uint16_t serverPort=atoi(argv[2]);

    int sockFd=socket(AF_INET,SOCK_STREAM,0);
    if(sockFd<0)
    {
        logMessage(FATAL,"socket() failed::%s %d",strerror(errno),sockFd);
        exit(SOCKET_ERR);
    }
    logMessage(DEBUG,"socket() success::%d",sockFd);

    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_port=serverPort;
    server.sin_family=AF_INET;
    inet_aton(serverIp.c_str(),&server.sin_addr);
    if(connect(sockFd,(const sockaddr*)&server,sizeof(server))==-1)
    {
        logMessage(FATAL,"connect() failed::%s: %d",strerror(errno),sockFd);
        exit(CONNECT_ERR);
    }
    logMessage(DEBUG,"connect() success.");

    string message;
    while(!quit)
    {
        message.clear();
        cout<<"Please enter";
        getline(cin,message);
        if(strcasecmp(message.c_str(),"quit")==0)
        {
            quit=true;
        }
    ssize_t sw=write(sockFd,message.c_str(),message.size());
    if(sw>0)
    {
        message.resize(BUFFER_SIZE);
        ssize_t sr=read(sockFd,(char*)message.c_str(),BUFFER_SIZE);
        if(sr>0)
        {
            message[sr]='\0';
        }
        if(strcasecmp(message.c_str(),"quit"))
        {
            cout<<"Server Echo"<<message<<endl;
        }
    }
    else if(sw<0)
    {
        logMessage(FATAL,"Client write() failed: %d : %s",sockFd,strerror(errno));
        break;
    }
    }
    close(sockFd);
    return 0;
}


但是以上的版本是单进程版本,当多客户尝试连接服务器时,会发现服务器只会对第一个连接到客户端进行响应。当客户端连接到服务器的时候,会调用 to_upper() 这个函数,但是它本质上是一个死循环,所以单进程进去之后就出不来了,后面的客户无论怎么连接都是无法响应的, 因为进程的唯一一个执行流在死循环内,这里就需要用到多进程或者多线程来处理这个问题了。

多进程版本一

我们只需要在刚刚出问题的地方做修改就好了,即在 to_upper() 这个函数这里。只需要在该函数之前创建子进程,将这个函数放在子进程里面去执行。

#include"tcp.hpp"

using std::string;
using std::cerr;
using std::endl;
class tcpServer
{
    public:
        tcpServer(uint16_t port,const string& ip="")
            :_port(port)
            ,_ip(ip){}
        ~tcpServer()
        {}
        void Init()
        {
            _LiSockFd=socket(AF_INET,SOCK_STREAM,0);
            if(_LiSockFd<0)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(SOCKET_ERR);
            }
            logMessage(DEBUG,"socket() success::%d",_LiSockFd);
            struct sockaddr_in local;
            memset(&local,0,sizeof(local));
            local.sin_family=AF_INET;
            local.sin_port=htons(_port);
            _ip.empty()?(local.sin_addr.s_addr=htonl(INADDR_ANY)):(inet_aton(_ip.c_str(),&local.sin_addr));
            
            if(bind(_LiSockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(BIND_ERR);
            }
            logMessage(DEBUG,"bind() success::%d",_LiSockFd);

            if(listen(_LiSockFd,5)==-1)
            {
                logMessage(FATAL,"listen() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(LISTEN_ERR);
            }
            logMessage(DEBUG,"listen() success::%d",_LiSockFd);
        }
        void to_upper(int sock,const string& clientIP,const uint16_t &clientPort)
        {
            assert(sock>0);
            assert(!clientIP.empty());
            
            char inbuffer[BUFFER_SIZE];
            while(true)
            {
                ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1);
                if(s>0)
                {
                    inbuffer[s]='\0';
                    if(strcasecmp(inbuffer,"quit")==0)
                    {
                        logMessage(DEBUG,"Client requests to quit: [%s: %d]",clientIP.c_str(),clientPort);
                        break;
                    }
                    logMessage(DEBUG,"to_upper before:[%s:%d]>> %s",clientIP.c_str(),clientPort,inbuffer);
                    for(int i=0;i<s;i++)
                    {
                        if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
                            inbuffer[i]=toupper(inbuffer[i]);
                    }
                    logMessage(DEBUG,"to_upper after:[%s:%d]>>%s",clientIP.c_str(),clientPort,inbuffer);
                    write(sock,inbuffer,strlen(inbuffer));
                }
                else if(s==0)
                {
                    logMessage(DEBUG,"Client has quited:[%s:%d],clientIP,clientPort");
                    break;
                }
                else{
                    logMessage(DEBUG,"Client [%s:%d] read::%s",clientIP.c_str(),clientPort,strerror(errno));
                    break; 
                }
            }
            close(sock);
            logMessage(DEBUG,"Service close %d sockFd",sock);
        }
        void start()
        {
            signal(SIGCHLD,SIG_IGN);
            while(true)
            {
                struct sockaddr_in peer;
                socklen_t peerlen=sizeof(peer);
                
                int serviceSockFd=accept(_LiSockFd,(struct sockaddr*)&peer,&peerlen);
                if(serviceSockFd==-1)
                {
                    logMessage(WARINING,"accept() failed::%s : %d",strerror(errno),_LiSockFd);
                    continue;
                }
                string peerIp=inet_ntoa(peer.sin_addr);
                uint16_t peerPort=ntohs(peer.sin_port);
                logMessage(DEBUG,"accept() success:: [%s: %d] | %d",peerIp.c_str(),peerPort,serviceSockFd);
                pid_t id=fork();
                if(id==-1)
                {
                    logMessage(FATAL,"Server fork() failed:%s",strerror(errno));
                    exit(FORK_ERR);
                }
                else if(id==0)
                {
                    // 进入子进程
                    // 子进程会继承 父进程的文件描述符表, 但是这已经是两个不同的进程了
                    // 所以建议进入子进程之后, 先关闭_LiSockFd, 防止子进程代码可能对此文件造成影响
                    close(_LiSockFd);
                    to_upper(serviceSockFd,peerIp,peerPort);
                    exit(0);
                }
                close(serviceSockFd);
                 // 父进程需要手动关闭 连接客户端创建的与客户端通信的文件描述符
                 // 如果不关闭父进程会发生文件描述符泄漏
                 // 父子进程在各子进程中关闭某文件描述符 是不影响对方的
            }
        }
    private:
        uint16_t _port;
        string _ip;
        int _LiSockFd;
};

void Usage(string proc)
{
    cerr<<"Usage::\n\t"<<proc<<"port ip"<<endl;
    cerr<<"example::\n\t"<<proc<<"8080 127.0.0.1"<<endl;
}

int main(int argc,char* argv[])
{
    if(argc!=3&&argc!=2)
    {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }
    tcpServer Usvr(port,ip);
    Usvr.Init();
    Usvr.start();
    return 0;
}

需要注意的问题:

  1. 子进程是继承父进程的文件描述符表的,也就是说父进程已经打开的文件在子进程继承的时候是默认打开的。
  2.  _LiSockFd 是监听套接字,对于子进程来说是没用的,所以提前关掉以免子进程的执行影响到父进程的监听。
  3. serviceSockFd是服务端与客户端用来通信的新的套接字,使用多进程后变成了子进程实现通信功能,该套接字的文件描述符的关闭操作原本是在 to_upper() 函数中的,现在在子进程中调用此函数,所以子进程内不用再手动关闭了。但是, 父进程已经不再执行to_upper(), 所以必须要 close(serviceSockFd) . 否则父进程会发生文件描述符泄漏
  4. 子进程退出时, 是需要父进程回收的.如果父进程使用 wait() 进行等待, 默认是阻塞式的 无法使用. 如果使用 waitpid() 非阻塞, 也需要存储所有子进程的pid, 很麻烦.所以, 我们直接在 start() 函数刚开始 执行 signal(SIGCHLD,SIG_IGN); 忽略子进程的退出信号. 这样子进程退出时, 会被自动回收.

多进程版本二

这个版本不在忽略子进程的退出信号. 而是利用了进程的一个特性.

如果子进程还没有退出, 但是其父进程退出了, 那么此子进程就成了孤儿进程, 会被操作系统领养. 退出时会自动被操作系统回收

即, 主父进程创建子进程之后, 子进程又创建了孙子进程. 然后子进程退出, 让孙子进程与客户端通信. 主父进程直接回收退出的子进程. 也不会发生一直阻塞等待的情况.

#include"tcp.hpp"

using std::string;
using std::cerr;
using std::endl;
class tcpServer
{
    public:
        tcpServer(uint16_t port,const string& ip="")
            :_port(port)
            ,_ip(ip){}
        ~tcpServer()
        {}
        void Init()
        {
            _LiSockFd=socket(AF_INET,SOCK_STREAM,0);
            if(_LiSockFd<0)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(SOCKET_ERR);
            }
            logMessage(DEBUG,"socket() success::%d",_LiSockFd);
            struct sockaddr_in local;
            memset(&local,0,sizeof(local));
            local.sin_family=AF_INET;
            local.sin_port=htons(_port);
            _ip.empty()?(local.sin_addr.s_addr=htonl(INADDR_ANY)):(inet_aton(_ip.c_str(),&local.sin_addr));
            
            if(bind(_LiSockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
            {
                logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(BIND_ERR);
            }
            logMessage(DEBUG,"bind() success::%d",_LiSockFd);

            if(listen(_LiSockFd,5)==-1)
            {
                logMessage(FATAL,"listen() failed::%s : %d",strerror(errno),_LiSockFd);
                exit(LISTEN_ERR);
            }
            logMessage(DEBUG,"listen() success::%d",_LiSockFd);
        }
        void to_upper(int sock,const string& clientIP,const uint16_t &clientPort)
        {
            assert(sock>0);
            assert(!clientIP.empty());
            
            char inbuffer[BUFFER_SIZE];
            while(true)
            {
                ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1);
                if(s>0)
                {
                    inbuffer[s]='\0';
                    if(strcasecmp(inbuffer,"quit")==0)
                    {
                        logMessage(DEBUG,"Client requests to quit: [%s: %d]",clientIP.c_str(),clientPort);
                        break;
                    }
                    logMessage(DEBUG,"to_upper before:[%s:%d]>> %s",clientIP.c_str(),clientPort,inbuffer);
                    for(int i=0;i<s;i++)
                    {
                        if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
                            inbuffer[i]=toupper(inbuffer[i]);
                    }
                    logMessage(DEBUG,"to_upper after:[%s:%d]>>%s",clientIP.c_str(),clientPort,inbuffer);
                    write(sock,inbuffer,strlen(inbuffer));
                }
                else if(s==0)
                {
                    logMessage(DEBUG,"Client has quited:[%s:%d],clientIP,clientPort");
                    break;
                }
                else{
                    logMessage(DEBUG,"Client [%s:%d] read::%s",clientIP.c_str(),clientPort,strerror(errno));
                    break; 
                }
            }
            close(sock);
            logMessage(DEBUG,"Service close %d sockFd",sock);
        }
        void start()
        {
            while(true)
            {
                struct sockaddr_in peer;
                socklen_t peerlen=sizeof(peer);
                
                int serviceSockFd=accept(_LiSockFd,(struct sockaddr*)&peer,&peerlen);
                if(serviceSockFd==-1)
                {
                    logMessage(WARINING,"accept() failed::%s : %d",strerror(errno),_LiSockFd);
                    continue;
                }
                string peerIp=inet_ntoa(peer.sin_addr);
                uint16_t peerPort=ntohs(peer.sin_port);
                logMessage(DEBUG,"accept() success:: [%s: %d] | %d",peerIp.c_str(),peerPort,serviceSockFd);
                pid_t id=fork();
                if(id==-1)
                {
                    logMessage(FATAL,"Server fork() failed:%s",strerror(errno));
                    exit(FORK_ERR);
                }
                else if(id==0)
                {
                    close(_LiSockFd);
                    if(fork()>0)
                    {
                        exit(0);
                    }
                    to_upper(serviceSockFd,peerIp,peerPort);
                    exit(0);
                }
                close(serviceSockFd);
                int ret=waitpid(id,nullptr,peerPort);
                if(ret==-1)
                {
                    logMessage(FATAL,"Server waitpid failed:%s",strerror(errno));
                    exit(WAIT_ERR);
                }
                (void)ret;
            }
        }
    private:
        uint16_t _port;
        string _ip;
        int _LiSockFd;
};

void Usage(string proc)
{
    cerr<<"Usage::\n\t"<<proc<<"port ip"<<endl;
    cerr<<"example::\n\t"<<proc<<"8080 127.0.0.1"<<endl;
}

int main(int argc,char* argv[])
{
    if(argc!=3&&argc!=2)
    {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }
    tcpServer Usvr(port,ip);
    Usvr.Init();
    Usvr.start();
    return 0;
}

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

相关文章:

  • 碳中和小程序:助力用户记录低碳行为,推动环保生活
  • Flutter 基础组件 Scaffold 详解
  • LabVIEW非线性拟合实现正弦波参数提取
  • 通过数据库网格架构构建现代分布式数据系统
  • 基于springboot+vue的佳途旅行分享预约平台
  • 第27周JavaSpringboot电商进阶开发 1.企业级用户验证
  • 《Python基础教程》附录A笔记:简明教程
  • 对Docker的一些基本认识
  • 用ABBYY PDF Transformer+对PDF的创建编辑转换和注释等操作
  • 埋点PV和UV的含义
  • PAT乙级(1101 B是A的多少倍)C语言解析
  • 五、非云原生监控mysql-Exporter
  • 【玩转23种Java设计模式】结构型模式篇:享元模式
  • QT小项目-简单的记事本
  • 1.5 双指针专题:有效三⻆形的个数(medium)
  • Flink之水印(watermark)的补充理解
  • Linux驱动开发-设备树
  • python高效试用17---两个字符串组成一个新的字符串和两个字符串组成元组作为key哪个更高效
  • PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)!
  • Qt不同窗口类的控件信号和槽绑定