【网络编程】实现一个简单多线程版本TCP服务器(附源码)

【网络编程】实现一个简单多线程版本TCP服务器(附源码)_第1张图片

TCP多线程

  • 预备知识
    • Accept函数
    • 字节序转换函数
    • listen函数
  • 代码
    • Log.hpp
    • Makefile
    • ☘️TCPClient.cc
    • TCPServer.cc
    • util.hpp

预备知识

Accept函数

accept 函数是在服务器端用于接受客户端连接请求的函数,它在监听套接字上等待客户端的连接,并在有新的连接请求到来时创建一个新的套接字用于与该客户端通信。

  • 下面是 accept 函数的详细介绍以及各个参数的意义:
#include 

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

sockfd: 是服务器监听套接字的文件描述符,通常是使用 socket 函数创建的套接字。accept 函数在该套接字上等待连接请求。

addr: 是一个指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。当新连接建立成功后,客户端的地址信息将会被填充到这个结构体中。

addrlen: 是一个指向 socklen_t 类型的指针,它指示 addr 结构体的长度。在调用 accept 函数之前,需要将其初始化为 addr 结构体的大小,函数执行后会更新为实际的客户端地址长度。

返回值:如果连接成功建立,accept 函数将返回一个新的文件描述符,该文件描述符用于与客户端进行通信。如果连接失败,函数将返回 -1,并设置 errno 以指示错误原因。

  • accept 函数的工作原理如下:

当服务器的监听套接字接收到一个新的连接请求时,accept 函数会创建一个新的套接字用于与该客户端通信。
新的套接字会继承监听套接字的监听属性,包括 IP 地址、端口等。
accept 函数会填充 addr 结构体,以便获取客户端的地址信息。
服务器可以使用返回的新套接字与客户端进行通信。

  • 注意事项:

accept 函数在没有连接请求时会阻塞,直到有新的连接请求到来。
如果希望设置非阻塞模式,可以使用 fcntl 函数设置 O_NONBLOCK 属性。
在多线程或多进程环境下,需要注意 accept 函数的线程安全性,可以使用互斥锁等机制来保护。
综上所述,accept 函数在构建服务器程序时非常重要,它使服务器能够接受客户端的连接请求并创建新的套接字与客户端进行通信。

字节序转换函数

在网络编程中,字节序问题很重要,因为不同的计算机体系结构可能使用不同的字节序,这可能导致在通信过程中的数据解释错误。为了在不同体系结构之间正确传递数据,需要进行字节序的转换。

  • 以下是一些常用的字节序转换函数:

ntohl 和 htonl: 这些函数用于 32 位整数的字节序转换。ntohl 用于将网络字节序转换为主机字节序,htonl 则相反,将主机字节序转换为网络字节序。

ntohs 和 htons: 这些函数用于 16 位整数的字节序转换。ntohs 用于将网络字节序转换为主机字节序,htons 则相反,将主机字节序转换为网络字节序。

这些函数通常用于在网络编程中处理套接字通信中的数据转换,以确保在不同平台上的正确数据交换。

  • 示例
#include 

int main() {
    uint32_t networkValue = 0x12345678;
    uint32_t hostValue = ntohl(networkValue); // 0x78563412 on a little-endian host
    uint32_t convertedValue = htonl(hostValue); // 0x12345678 on a little-endian host

    uint16_t networkPort = 0x1234;
    uint16_t hostPort = ntohs(networkPort); // 0x3412 on a little-endian host
    uint16_t convertedPort = htons(hostPort); // 0x1234 on a little-endian host

    return 0;
}

请注意,在使用这些函数时,需要包含 头文件。这些函数通常在网络编程中用于正确处理字节序问题,以确保不同平台之间的数据传输正确。

listen函数

在TCP通信中,服务端需要使用 listen 函数来监听连接请求。这是因为TCP是一种面向连接的协议,它采用客户端-服务端模型进行通信,通信双方需要先建立连接,然后进行数据的传输。监听的过程是为了等待客户端发起连接请求。

  • 具体原因如下:

建立连接: 在TCP通信中,通信双方需要通过三次握手建立连接。客户端通过 connect 函数向服务器发起连接请求,而服务端则需要通过 listen 函数来准备接收连接请求。

处理并发连接: 服务端可能会同时接收多个客户端的连接请求,而每个连接都需要为其分配一个独立的套接字。通过监听连接请求,服务端可以在一个循环中接受多个连接,为每个连接创建对应的套接字,从而实现并发处理多个客户端。

连接队列: listen 函数将连接请求存储在一个队列中,等待服务端逐个接受。这个队列称为“未完成连接队列”(backlog queue)。如果连接请求过多,超出了队列的长度,那么新的连接请求可能会被拒绝或被丢弃。

连接参数: listen 函数还可以指定一个参数,表示在未完成连接队列中可以容纳的连接请求数量。这个参数可以影响服务端处理并发连接的能力。

总之,TCP监听是为了等待客户端发起连接请求,建立连接,然后实现双方的数据传输。这种机制允许服务器处理多个客户端连接,实现高并发的网络服务。

  • 函数原型:
int listen(int sockfd, int backlog);

  • 参数说明:

sockfd:要进行监听的套接字描述符。
backlog:表示在未完成连接队列中可以容纳的连接请求数量。这个参数可以影响服务器处理并发连接的能力。通常情况下,系统会为这个值设置一个默认的最大值,但你也可以根据你的需求进行适当调整。
返回值:
如果函数调用成功,返回 0。
如果出现错误,返回 -1,并设置全局变量 errno 来指示错误类型。

使用步骤:

创建套接字并绑定地址。
调用 listen 函数将套接字标记为被动套接字,开始监听连接请求。
使用 accept 函数接受客户端连接请求,建立实际的连接。

  • 示例用法
#include 
#include 
#include 
#include 
#include 

int main() {
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(listen_sock, 5) == -1) { // 开始监听,最多允许5个未完成连接
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 现在可以使用 accept 函数接受连接请求并建立连接

    close(listen_sock);
    return 0;
}

注意:listen 后的套接字仅能用于接受连接请求,不能用于读写数据。接收到的连接请求将在一个队列中等待,直到使用 accept 函数从队列中取出并建立连接。

代码

Log.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

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

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);

    va_end(ap); // ap = NULL


    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);

    // char *s = format;
    // while(s){
    //     case '%':
    //         if(*(s+1) == 'd')  int x = va_arg(ap, int);
    //     break;
    // }
}

Makefile

.PHONY:all
all:TCPClient TCPServer

TCPClient: TCPClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
TCPServer:TCPServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f TCPClient TCPServer

☘️TCPClient.cc

#include"util.hpp"
volatile bool quit=false;
static void Usage(std::string proc)
{
    std::cerr<<"Usage:\n\t"<<proc<<"serverip serverport "<<std::endl;
    std::cerr<<"Example:\n\t"<<proc<<"127.0.0.1 8080\n"<<std::endl;
}

int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);
    //1.创建socket SOCK_STREAM
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        std::cerr<<"socket :"<<strerror(errno)<<std::endl;
        exit(SOCKET_ERR);
    }
    //2.链接 
    //向服务器发起链接请求
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(server.sin_port);
    inet_aton(serverip.c_str(),&server.sin_addr);

    //2.2发起请求 connect自动会进行bind
    if(connect(sock,(const struct sockaddr*)&server,sizeof(server))!=0)
    {
        //链接失败
        std::cerr<<"connect :"<<strerror(errno)<<std::endl;
        exit(CONN_ERR);
    }
    //链接成功
    std::cout<<" info :connect success :"<<sock<<std::endl;

    std::string message;


    while(!quit)
    {
        message.clear();
        std::cout<<"请输入您的消息>>>>"<<std::endl;
        std::getline(std::cin,message);
        if(strcasecmp(message.c_str(),"quit")==0)
        {
            //如果输入的是quit 直接退出程序
            quit=true; //设置成true 会把当前信息先执行发送到服务器 再进入while循环时条件不满直接退出

        }

        //从服务器接收到的消息
        ssize_t s=write(sock,message.c_str(),message.size());
        
        if(s>0)
        {
            message.resize(1024);
            ssize_t s=read(sock,(char *)(message.c_str()),1024);
            if(s>0)
                message[s]=0;
            std::cout<<"Server Echo>>>"<<"message"<<std::endl;
        }
         else if (s <= 0)
        {
            break;
        }
    }
    close(sock);
    
    return 0;
}

TCPServer.cc

#include "util.hpp"
#include 
#include 
#include 
#include 
class ServerTcp;//先声明

class ThreadData
{
    public:
    uint16_t clientPort_;//客户端端口号
    std::string clientip_;//客户端ip
    int sock_;
    ServerTcp *this_;

     ThreadData(uint16_t port, std::string ip, int sock,  ServerTcp *ts)
        : clientPort_(port), clientip_(ip), sock_(sock),this_(ts)
    {}

};


class ServerTcp
{

    public:
    //构造和和析构函数
    ServerTcp(uint16_t port,const std::string &ip=""):port_(port),ip_(ip),listenSock_(-1)
    {}
    ~ServerTcp()
    {}
    


    public:
    //初始化函数
    void init()
    {

        //第一步:创建套接字
        listenSock_=socket(PF_INET,SOCK_STREAM,0);

        if(listenSock_<0)
        {
            //创建失败
            logMessage(FATAL,"socket:%s",strerror(errno)); //用日志打印错误信息
            exit(SOCKET_ERR);
        }
        //创建成功
        logMessage(DEBUG,"sockt:%s,%d",strerror(errno),listenSock_);

        //第二步 bind绑定
        //2.1填充服务器信息

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));//设置0?
        /*可以确保将所有这些字段初始化为零,以避免在实际使用过程中出现未定义行为或不可预测的结果。*/
        local.sin_family=AF_INET;   
        /*如果 ip_ 为空,服务器将绑定到任意可用的本地IP地址。如果 ip_ 不为空,服务器将绑定到 ip_ 所代表的具体IP地址。*/
        ip_.empty()?(local.sin_addr.s_addr)=htons(INADDR_ANY):(inet_aton(ip_.c_str(),&local.sin_addr));
        
        //2.2
        if(bind(listenSock_,(const struct sockaddr*)&local,sizeof local)<0)//
        {
            //bind绑定失败
            logMessage(FATAL,"bind:%s",strerror(errno));
            exit(BIND_ERR);
        }

        //绑定成功
        logMessage(DEBUG,"bind:%S,%d",strerror(errno),listenSock_);
        //3.监听socket
        if(listen(listenSock_,5)<0)
        {
            logMessage(FATAL,"listen:%s",strerror(errno));
            exit(LISTEN_ERR);
        }
        //监听成功
        logMessage(DEBUG,"listen:%S,%d",strerror(errno),listenSock_);
        //到这一步就等待运行 等待客户端链接
 
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); //设置线程分离
        ThreadData *td = static_cast<ThreadData*>(args);
        td->this_->tranService(td->sock_, td->clientip_, td->clientPort_);
        delete td;
        return nullptr;
    }
    
    //加载
    void loop()
    {
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            //获取链接 accept返回值??
            int serviceSock=accept(listenSock_,(struct sockaddr*)&peer,&len);
            if(serviceSock<0)
            {
                //获取连接失败
                logMessage(WARINING,"Accept :%S[%d]",strerror(errno),serviceSock);
                continue;//获取失败 继续接收....
            }

            //获取客户端的基本信息 存储起来
             uint16_t peerPort=ntohs(peer.sin_port);
             std::string peerip=inet_ntoa(peer.sin_addr);
            //打印一下获取的客户端信息
             logMessage(DEBUG,"Aceept :%s|%s[%d],socket fd :%d",strerror(errno),peerip.c_str(),peerPort,serviceSock);


               // 5 提供服务, echo -> 小写 -> 大写
            // 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
            // transService(serviceSock, peerIp, peerPort);
            
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     close(listenSock_); //建议
            //     //子进程
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0); // 进入僵尸
            // }
            // // 父进程
            // close(serviceSock); //这一步是一定要做的!

            // 5.1 v1.1 版本 -- 多进程版本  -- 也是可以的
            // 爷爷进程
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 爸爸进程
            //     close(listenSock_);//建议
            //     // 又进行了一次fork,让 爸爸进程
            //     if(fork() > 0) exit(0);
            //     // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0);
            // }
            // // 父进程
            // close(serviceSock); //这一步是一定要做的!
            // // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
            // pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
            // assert(ret > 0);
            // (void)ret;

            // 5.2 v2 版本 -- 多线程
            // 这里不需要进行关闭文件描述符吗??不需要啦
            // 多线程是会共享文件描述符表的!
            ThreadData *td = new ThreadData(peerPort, peerip, serviceSock, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, (void*)td);

            // waitpid(); 默认是阻塞等待!WNOHANG
            // 方案1

            // logMessage(DEBUG, "server 提供 service start ...");
            // sleep(1);
        }

    }


     //提供服务函数 -----> 大小写转换
     void tranService(int sock,const std::string &clientip,uint16_t clientPort)
     {
            assert(sock>=0);
            assert(!clientip.empty());
            assert(clientPort>=1024); //1~~1024端口为系统端口 不可轻易更改
            char inbuffer[BUFFER_SIZE];
            while(true)
            {
                ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1); //-1是给\0留出一个位置
                if(s>0)
                {
                    inbuffer[s]='0';
                    if(strcasecmp(inbuffer,"quit")==0)
                    {
                        logMessage(DEBUG,"client quit----------%s[%d]",clientip.c_str(),clientPort);
                        break;
                    }
                    logMessage(DEBUG,"Treans 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,"Trans after:%s[%d]>>>>%s",clientip.c_str(),clientPort,inbuffer);
                
                        write(sock,inbuffer,strlen(inbuffer));//给客户端发送回去
                }
                    else if(s==0)
                    {
                    // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
                     // s == 0: 代表对方关闭,client 退出
                    logMessage(DEBUG, "client quit -- %s[%d]", clientip.c_str(), clientPort);
                    break;
                    }
                    else
                     {
                    logMessage(DEBUG, "%s[%d] - read: %s", clientip.c_str(), clientPort, strerror(errno));
                    break;
                     }
 
                }
                // 只要走到这里,一定是client退出了,服务到此结束
        close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
        logMessage(DEBUG, "server close %d done", sock);
            }
          
    private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
};



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

}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if(argc != 2 && argc != 3 )
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if(argc == 3) ip = argv[2];

    ServerTcp svr(port, ip);
    svr.init();
    svr.loop();
    return 0;
}

util.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5

#define BUFFER_SIZE 1024

大家可以拉下来自行测试…

你可能感兴趣的:(Linux,网络,tcp/ip,服务器)