TCP实现服务器模型三种方式

一、服务器模型

        在网络程序中往往一个服务器对多个客户机,为处理客户端请求,服务器有不同处理方法;

        目前常用服务器模型:

                循环服务器:循环服务器在同一时刻只能响应一个客户端,TCP默认为循环服务器,原因是accept与recv函数影响。

                并发服务器:并发服务器在同一时刻可以响应多个客户端,UDP默认为并发服务器,原因是只有一个阻塞函数recvfrom。

二、如何实现TCP并发服务器

        1.多进程实现TCP并发服务器;

        2.多线程实现TCP并发服务器;

        3.IO多路复用实现TCP并发服务器;

三种方式共用客户端:  

#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_LOG(msg) do{\
    printf("%s %s %d\n",__func__,__FILE__,__LINE__);\
    perror(msg);\
    exit(-1);\
}while(0)

#define N 128

int main(int argc,const char *argv[])
{
    if(argc!=3){
        printf("Usage:%s <IP> <PORT>\n",argv[0]);
        exit(-1);
    }
    int sockfd=0;
    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
        ERR_LOG("socket error");
    
    struct sockaddr_in serveraddr;
    socklen_t serveraddr_len=sizeof(serveraddr);
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr=inet_addr(argv[1]);

    if((connect(sockfd,(struct sockaddr *)&serveraddr,serveraddr_len))==-1)
        ERR_LOG("connect error");
    
    char buff[N]={0};
    while(1){
        memset(buff,0,N);
        fgets(buff,N,stdin);
        buff[strlen(buff)-1]='\0';
        if((send(sockfd,buff,N,0))==-1)
            ERR_LOG("send error");
        if((recv(sockfd,buff,N,0))==-1)
            ERR_LOG("recv error");
        printf("%s\n",buff);
    }
    close(sockfd);
    return 0;
}

1.1 多进程实现TCP并发服务器

        主进程接收客户端请求,子进程负责收发数据。

        服务器端代码:

#include <arpa/inet.h> 
#include <netinet/in.h> 
#include <netinet/ip.h> /* superset of previous */ 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <sys/socket.h> 
#include <sys/types.h> /* See NOTES */ 
#include <unistd.h> 
#include <signal.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#define ERR_LOG(msg)                                        \ 
    do {                                                    \ 
        printf("%s %s %d\n", __func__, __FILE__, __LINE__); \ 
        perror(msg);                                        \ 
        exit(-1);                                           \ 
    } while (0) 
 
#define N 128 
 
void sig_func(int sig) 
{ 
    if(sig == SIGCHLD) 
    { 
        wait(NULL); 
    } 
} 
 
int main(int argc, char const* argv[]) 
{ 
    if (argc != 3) { 
        printf("Usage: %s <IP> <PORT>\n", argv[0]); 
        exit(-1); 
    } 
    // 1.创建套接字 
    int sockfd = 0; 
    int acceptfd = 0; 
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 
        ERR_LOG("socket error"); 
    } 
    printf("sockfd = %d\n", sockfd); 
 
    // 2. 填充网络信息结构体 
    struct sockaddr_in serveraddr; 
    struct sockaddr_in clientaddr; 
 
    socklen_t clientaddr_len = sizeof(clientaddr); 
    socklen_t serveraddr_len = sizeof(serveraddr); 
    memset(&serveraddr, 0, sizeof(serveraddr)); 
    memset(&clientaddr, 0, sizeof(clientaddr)); 
 
    serveraddr.sin_family = AF_INET; 
    // 使用命令行传参: ./server 192.168.1.106 8888 
    // 配置文件 
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100 
    serveraddr.sin_port = htons(atoi(argv[2])); 
 
    // 3. 绑定网络信息结构体 
    if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) { 
        ERR_LOG("bind error"); 
    } 
 
    // 4. 使套接字处于被动监听状态 
    if (listen(sockfd, 5) == -1) { 
        ERR_LOG("listen error"); 
    } 
 
    int pid; 
    char buff[N] = { 0 }; 
    int ret = 0; 
    printf("服务器正在运行...\n"); 
 
    while (1) { 
        // 5. 阻塞等待连接 
        if ((acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len)) == -1) { 
            ERR_LOG("accept error"); 
        } 
        printf("客户端[%s]:[%d]连接了...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); 
 
        //------------多进程实现TCP并发服务器-------------- 
        if ((pid = fork()) < 0) { 
            ERR_LOG("fork error"); 
        } else if (pid > 0) { 
            // 父进程    
            close(acceptfd); //回收资源防止浪费
            signal(SIGCHLD,sig_func); 
        } else { 
            // 子进程 
            close(sockfd); //回收资源防止浪费
            while (1) { 
                memset(buff, 0, sizeof(buff)); 
                if ((ret = recv(acceptfd, buff, N, 0)) == -1) { 
                    ERR_LOG("recv error"); 
                } else if (ret == 0) { 
                    printf("客户端[%s]:[%d]退出...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); 
                    close(acceptfd); 
                    break; 
                } 
 
                if (strcmp(buff, "quit") == 0) { 
                    printf("客户端[%s]:[%d]退出...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); 
                    close(acceptfd); 
                    break; 
                } 
 
                printf("buff=%s\n", buff); 
 
                strcat(buff, "^_^"); 
                if (send(acceptfd, buff, N, 0) == -1) { 
                    ERR_LOG("send error"); 
                } 
            } 
            //退出子进程 
            exit(0); 
        } 
        
    } 
 
    close(acceptfd); 
    close(sockfd); 
 
    return 0; 
} 

多进程在服务器代码中需要注意一下几点:

1.父进程负责为子进程回收资源,但不能使用wait(默认阻塞),waitpid(需要在轮询中不断检测子进程退出),所以选择使用信号捕捉为子进程回收资源。

2.accept阻塞等待客户端来电,需要在循环中进行,不然只能接收一次客户端请求。

3.子进程退出必须使用exit(0);因为不使用exit子进程退出会进入循环再次执行循环代码。

1.2 多线程实现TCP并发服务器

        主线程处理客户端的请求,子线程负责处理收发数据。

        服务器端代码:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
#define ERR_LOG(msg)                                        \
    do {                                                    \
        printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
        perror(msg);                                        \
        exit(-1);                                           \
    } while (0)
 
#define N 128
 
typedef struct {
    int acceptfd;
    struct sockaddr_in clientaddr;
} Msg_t;
 
// 线程处理函数
void* pthread_func(void* arg)
{
    int ret = 0;
    Msg_t msg = *((Msg_t*)arg);
    char buff[N] = {0};
    while (1) {
        memset(buff, 0, sizeof(buff));
        if ((ret = recv(msg.acceptfd, buff, N, 0)) == -1) {
            printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
            perror("recv error");                                        \
            pthread_exit(NULL);
        } else if (ret == 0) {
            printf("客户端[%s]:[%d]退出...\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
            close(msg.acceptfd);
            break;
        }
 
        if (strcmp(buff, "quit") == 0) {
            printf("客户端[%s]:[%d]退出...\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
            close(msg.acceptfd);
            break;
        }
 
        printf("buff=%s\n", buff);
 
        strcat(buff, "^_^");
        if (send(msg.acceptfd, buff, N, 0) == -1) {
            printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
            perror("send error");                                        \
            pthread_exit(NULL);
        }
    }
    pthread_exit(NULL);
}
 
int main(int argc, char const* argv[])
{
    if (argc != 3) {
        printf("Usage: %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }
    // 1.创建套接字
    int sockfd = 0;
    int acceptfd = 0;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        ERR_LOG("socket error");
    }
    printf("sockfd = %d\n", sockfd);
 
    // 2. 填充网络信息结构体
    struct sockaddr_in serveraddr;
    struct sockaddr_in clientaddr;
 
    socklen_t clientaddr_len = sizeof(clientaddr);
    socklen_t serveraddr_len = sizeof(serveraddr);
    memset(&serveraddr, 0, sizeof(serveraddr));
    memset(&clientaddr, 0, sizeof(clientaddr));
 
    serveraddr.sin_family = AF_INET;
    // 使用命令行传参: ./server 192.168.1.106 8888
    // 配置文件
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100
    serveraddr.sin_port = htons(atoi(argv[2]));
 
    // 3. 绑定网络信息结构体
    if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) {
        ERR_LOG("bind error");
    }
 
    // 4. 使套接字处于被动监听状态
    if (listen(sockfd, 5) == -1) {
        ERR_LOG("listen error");
    }
 
    int pid;
    char buff[N] = { 0 };
    int ret = 0;
    pthread_t tid;
    Msg_t msg;
    printf("服务器正在运行...\n");
 
    while (1) {
        // 5. 阻塞等待连接
        if ((acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len)) == -1) {
            ERR_LOG("accept error");
        }
        printf("客户端[%s]:[%d]连接了...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
 
        msg.acceptfd = acceptfd;
        msg.clientaddr = clientaddr;
        //------------多线程实现TCP并发服务器--------------
        pthread_create(&tid, NULL, pthread_func, (void*)&msg);
        pthread_detach(tid);
    }
 
    close(acceptfd);
    close(sockfd);
 
    return 0;
}

多线程在服务器代码中需要注意一下几点:

1.在创建线程执行函数时,线程需要acceptfd和客户端网络信息结构体,所以必须传递结构体来实现。

2.第二个注意的点是必须Msg_t msg = *((Msg_t*)arg);来使用,不能定义指针msg_t* msg=(msg_t*)arg; 这种方法msg的网络信息结构体一直在变化,有其他客户端连接进来可能会导致前面的客户端不能使用。

3.第三点要将子线程设置为分离态,让系统回收子线程资源,主线程负责循环等待客户端请求。

4.在子线程中只能使用pthread_exit(NULL);退出。

1.3 IO多路复用实现TCP并发服务器

        服务器代码:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define ERR_LOG(msg)                                        \
    do {                                                    \
        printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
        perror(msg);                                        \
        exit(-1);                                           \
    } while (0)
 
#define N 128
 
int main(int argc, char const* argv[])
{
    if (argc != 3) {
        printf("Usage: %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }
    // 1.创建套接字
    int sockfd = 0;
    int acceptfd = 0;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        ERR_LOG("socket error");
    }
 
    // 2. 填充网络信息结构体
    struct sockaddr_in serveraddr;
    struct sockaddr_in clientaddr;
 
    socklen_t clientaddr_len = sizeof(clientaddr);
    socklen_t serveraddr_len = sizeof(serveraddr);
    memset(&serveraddr, 0, sizeof(serveraddr));
    memset(&clientaddr, 0, sizeof(clientaddr));
 
    serveraddr.sin_family = AF_INET;
    // 使用命令行传参: ./server 192.168.1.106 8888
    // 配置文件
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100
    serveraddr.sin_port = htons(atoi(argv[2]));
 
    // 3. 绑定网络信息结构体
    if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) {
        ERR_LOG("bind error");
    }
 
    // 4. 使套接字处于被动监听状态
    if (listen(sockfd, 5) == -1) {
        ERR_LOG("listen error");
    }
 
    printf("服务器正在运行...\n");
    // -------------- 使用select实现TCP并发服务器---------
    //---- 使用select 实现超时检测------
    char buff[N] = {0};
    int ret = 0;
 
    //设置超时时间5s
    struct timeval tm;
 
    //1. 构建一张表
    fd_set readfds;
    fd_set readfds_temp;
    int nfds = 0;
    FD_ZERO(&readfds);
    FD_ZERO(&readfds_temp);
    //填充表
 
    FD_SET(sockfd,&readfds);
    nfds = sockfd > nfds ? sockfd:nfds;
 
    while(1){
        tm.tv_sec = 5;
        tm.tv_usec = 0;
        readfds_temp  = readfds;
        //使用select 阻塞等待文件描述符准备就绪
        if((ret=select(nfds+1,&readfds_temp,NULL,NULL,&tm))==-1)
        {
            ERR_LOG("select error");
        }else if (ret == 0)
        {
            printf("timeout...\n");
            continue;
        }
 
        //select 就代表有文件描述符准备就绪
        for(int i=3;i<nfds+1;i++)
        {
            if(FD_ISSET(i,&readfds_temp))
            {
                if(i == sockfd)
                {
                    //有新的客户端连接
                    if((acceptfd = accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddr_len))==-1)
                    {
                        ERR_LOG("accept error");
                    }
                    printf("[%d]连接了...\n",acceptfd);
                    //需要将acceptfd放入文件描述符的集合中
                    FD_SET(acceptfd,&readfds);
                    nfds = nfds > acceptfd? nfds:acceptfd;
 
                }else{
                    //客户端发来数据
                    if((ret=recv(i,buff,sizeof(buff),0))==-1)
                    {
                        ERR_LOG("recv error");
                    }else if(ret == 0)
                    {
                        //对端关闭
                        printf("[%d]退出了...\n",i);
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    }
 
                    if(strcmp(buff,"quit")==0)
                    {
                        //对端关闭
                        printf("[%d]退出了...\n",i);
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    }
 
                    strcat(buff,"---hqyj");
 
                    if(send(i,buff,sizeof(buff),0)==-1)
                    {
                        ERR_LOG("send error");
                    }
                }
            }
        }
    }
    close(acceptfd);
    close(sockfd);
 
    return 0;
}

IO多路复用在服务器代码中需要注意一下几点:

1.select 的返回值为已经准备好的客户端文件描述符,返回0表示在规定时间内无客户端装备好,延时结束。

2.整个代码逻辑为:当有客户端准备好,select监听到后,在for循环中依次遍历找到被置为1的那一个文件描述符,这个文件描述符有可能是sockfd,也有可能是acceptfd,所以进行if  else  判断,如果是sockfd表示客户端第一次登录但没有发生信息,随后将acceptfd放入字符表中,当第二次客户端发送消息时,else代码负责进行数据的接收和发送。        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值