多线程服务器

 



在真实的服务器客户端模型中,服务器和客户端的关系应为1:n

单线程

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

int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 2. 将socket()返回值和本地的IP端口绑定到一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值为0 == 0.0.0.0
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    // 3. 设置监听
    ret = listen(lfd, 128);
    // 4. 阻塞等待并接受客户端连接
    struct sockaddr_in cliaddr;
    int clilen = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
    // 5. 和客户端通信
    while(1)
    {
        // 接收数据
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int len = read(cfd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("客户端say: %s\n", buf);
            write(cfd, buf, len);
        }
        else if(len  == 0)
        {
            printf("客户端断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(cfd);
    close(lfd);
    return 0;
}

在上面的代码中用到了三个会引起程序阻塞的函数,分别是:

  1. accept():如果服务器端没有新客户端连接,阻塞当前进程/线程,如果检测到新连接解除阻塞,建立连接
  2. read():如果通信的套接字对应的读缓冲区没有数据,阻塞当前进程/线程,检测到数据解除阻塞,接收数据
  3. write():如果通信的套接字写缓冲区被写满了,阻塞当前进程/线程(这种情况比较少见)

如果需要和发起新的连接请求的客户端建立连接,那么就必须在服务器端通过一个循环调用accept()函数,
另外已经和服务器建立连接的客户端需要和服务器通信,发送数据时的阻塞可以忽略,当接收不到数据时程序也会被阻塞,
这时候就会非常矛盾,被accept()阻塞就无法通信,被read()阻塞就无法和客户端建立新连接。
因此得出一个结论,基于上述处理方式,在单线程/单进程场景下,服务器是无法处理多连接的,解决方案也有很多,常用的有三种:

  1. 使用多线程实现
  2. 使用多进程实现
  3. 使用IO多路转接(复用)实现
  4. 使用IO多路转接 + 多线程实现

多线程

accept()函数,是一个线程,用于处理与客户端的连接
而每次建立连接,就需要多分配出一个线程来处理和客户端的通信

  • 主线程:
    • 负责监听,处理客户端的连接请求,也就是在父进程中循环调用accept()函数
    • 创建子线程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
    • 回收子线程资源:由于回收需要调用阻塞函数,这样就会影响accept(),直接做线程分离即可。
  • 子线程:负责通信,基于主线程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
    • 发送数据:send() / write()
    • 接收数据:recv() / read()

      注意:
  1. 同一地址空间中的多个线程的栈空间是独占的
  2. 多个线程共享全局数据区,堆区,以及内核区的文件描述符等资源,因此需要注意数据覆盖问题,并且在多个线程访问共享资源的时候,还需要进行线程同步。
#include <cstdio>
#include<arpa/inet.h>
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>

using namespace std;

void* working(void* arg);

#define MAXCLIENT 512


struct SockInfo
{
    int fd;                      // 通信
    pthread_t tid;               // 线程ID
    struct sockaddr_in addr;     // 地址信息
};

struct SockInfo infos[MAXCLIENT];

int main()
{
    //初始化
    for (int i = 0; i < MAXCLIENT; i++)
    {
        bzero(infos, 0);
        infos[i].fd = -1;
    }
    printf("%s 向你问好!\n", "SocketLinnx");
    //1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }
    //2.绑定本地端口
    //2.1   addr结构体的初始化和地址
    sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;//指定为0后,自动读取实际ip
    int ret = bind(fd, (sockaddr*)&saddr, sizeof(saddr));
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }
    //3.设置监听
    ret = listen(fd, 128);
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }
    //4.阻塞并等待客户端连接
    int addrlen = sizeof(sockaddr_in);
    while (1)
    {
        struct SockInfo* pinfo;
        for (int i = 0; i < MAXCLIENT; i++)
        {
            if (infos[i].fd == -1)
            {
                pinfo = &infos[i];
                break;
            }
        }
        pinfo->fd = accept(fd, (struct sockaddr*)&pinfo->addr, (socklen_t*)&addrlen);
        if (pinfo->fd == -1)
        {
            perror("accept");
            //连接失败了就进入下一次循环
            continue;
        }
        //创建对应子线程
        pthread_t tid;
        pthread_create(&tid, NULL, working, (void *)pinfo);
        pthread_detach(tid);
    }
    close(fd);
    return 0;
}
void * working(void* arg)
{
    //建立连接成功,打印客户端IP和端口
    //caddr是大端信息,需要转成小端输出
    struct SockInfo* pinfo = (struct SockInfo*)arg;

    char ip[32];
    printf("客户端IP:%s ,端口:%d\n", inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port));
    //5.进行通信
    while (true)
    {
        //接收数据
        char buff[1024];
        int len = recv(pinfo->fd, buff, sizeof(buff), 0);
        if (len > 0)
        {
            cout << buff << endl;
            send(pinfo->fd, buff, len, 0);
        }
        else if (len == 0)
        {
            cout << "客户端断开连接" << endl;
            break;
        }
        else {
            perror("recv");
            break;
        }
    }
    //释放文件描述符和其他的东西
    close(pinfo->fd);
    pinfo->fd = -1;
    return NULL;

}
posted @   LiviaYu  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示