前言: 刚开始学网络编程, 都会先写一个客户端和服务端, 不知道你们有没有试一下: 再打开一下客户端, 是连不上服务端的. 还有一个问题不知道你们发现没: 有时启服务器, 会提示 "Address already in use", 过一会就好了, 想过为啥么? 在这篇博客会解释这个问题.
但现实的服务器都会连很多客户端的, 像阿里服务器等, 所以这篇主要介绍如何实现并发服务器, 主要通过三种方式: 进程, 线程和 select 函数来分别实现.
一, 进程实现并发服务器
先说下什么是并发服务器吧? 不是指有多个服务器同时运行, 而是可以同时连接多个客户端.
先简单说下原理吧, 先画个图, 如下: PS: 全博客园最丑图, 不接受反驳! 哈哈哈
先要搞清楚通信的流程, 图上参数说明:
lfd:socket 函数的返回值, 就是监听描述符
cfd1/cfd2/cfd3:accept 函数的返回值, 用通信的套接字
server: 服务器
client: 客户端
socket 通信过程中, 总共有几个套接字呢? 答: 三个, 客户端一个, 服务器两个.
根据上图来大致说明一下流程:
客户端创建一个套接字描述符, 用于通信, 服务器先用 socket 函数创建套接字, 用于监听客户端, 然后调用 accept 函数, 会返回一个套接字, 用于通信的. 图上就是, client1 先通过 cfd 与 server 建立连接, 然后与 cfd1 建立连接通信, 这时 lfd 就空闲了, 再监听客户端, client2 再与 lfd 连接, 再跟 cfd2 通信. client3 也是如此.
现在问题就是. 如何创建多个进程与客户端通信呢? 通过循环创建子进程就可以实现这个问题, 可以参考我的这篇博客: https://www.cnblogs.com/liudw-0215/p/9667686.HTML
服务端程序, 如下:
- #include <stdio.h>
- #include <string.h>
- #include <netinet/in.h>
- #include <arpa/.NET.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <ctype.h>
- #include <unistd.h>
- #include "wrap.h"
- #define MAXLINE 8192
- #define SERV_PORT 8000
- void do_sigchild(int num)
- {
- while (waitpid(0, NULL, WNOHANG)> 0)
- ;
- }
- int main(void)
- {
- struct sockaddr_in servaddr, cliaddr;
- socklen_t cliaddr_len;
- int listenfd, connfd;
- char buf[MAXLINE];
- char str[INET_ADDRSTRLEN];
- int i, n;
- pid_t pid;
- struct sigaction newact;
- newact.sa_handler = do_sigchild;
- sigemptyset(&newact.sa_mask);
- newact.sa_flags = 0;
- sigaction(SIGCHLD, &newact, NULL); // 建立信号, 处理子进程退出
- listenfd = Socket(AF_INET, SOCK_STREAM, 0);
- // int opt = 1;
- //setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口复用
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
- Listen(listenfd, 20);
- printf("Accepting connections ...\n");
- while (1) {
- cliaddr_len = sizeof(cliaddr);
- connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
- pid = fork();
- if (pid == 0) {
- Close(listenfd);
- while (1) {
- n = Read(connfd, buf, MAXLINE);
- if (n == 0) {
- printf("the other side has been closed.\n");
- break;
- }
- printf("received from %s at PORT %d\n",
- inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
- for (i = 0; i <n; i++)
- buf[i] = toupper(buf[i]);
- Write(STDOUT_FILENO, buf, n);
- Write(connfd, buf, n);
- }
- Close(connfd);
- return 0;
- } else if (pid> 0) {
- Close(connfd);
- } else
- perr_exit("fork");
- }
- return 0;
- }
- View Code
客户端程序如下:
- /* client.c */
- #include <stdio.h>
- #include <string.h>
- #include <unistd.h>
- #include <netinet/in.h>
- #include <arpa/.NET.h>
- #include "wrap.h"
- #define MAXLINE 8192
- #define SERV_PORT 8000
- int main(int argc, char *argv[])
- {
- struct sockaddr_in servaddr;
- char buf[MAXLINE];
- int sockfd, n;
- sockfd = Socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
- servaddr.sin_port = htons(SERV_PORT);
- Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
- while (fgets(buf, MAXLINE, stdin) != NULL) {
- Write(sockfd, buf, strlen(buf));
- n = Read(sockfd, buf, MAXLINE);
- if (n == 0) {
- printf("the other side has been closed.\n");
- break;
- }
- else
- Write(STDOUT_FILENO, buf, n);
- }
- Close(sockfd);
- return 0;
- }
- View Code
演示效果, 服务器可以同时处理两个客户端, 如下:
但我想再测试一下程序, 执行./server, 发现有个 bind error, 如下:
下面来解释一下这个问题:
先来一张图片 (出自 UNP), 如下:
这张图将三次握手, 四次挥手和 TCP 状态转换图, 这些在我的这篇博客都由介绍, 可以参考一下: https://www.cnblogs.com/liudw-0215/p/9661583.HTML
注意最后有一个 TIME_WAIT 状态, 主动关闭一端会经历 2MSL 时长等待 (大约 40 秒), 再变为最开始的状态 CLOSED.
复现上面的 "bind error", 只需退出服务器, 在启服务器, 就会报出此错. 因为主动关闭一端, 会经历 2MSL 时长, 端口 IP 会被占用, 所以会报 "bind error".
但可能会问: 为啥先退出客户端没有此问题? 因为客户端没有调用 bind 函数地址结构, 会 "隐式" 生成端口.
有没有方法可以解决这个问题呢? 当然有的, 调用函数 setsockopt 即可, 服务端程序如下:
- #include <stdio.h>
- #include <string.h>
- #include <netinet/in.h>
- #include <arpa/.NET.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <ctype.h>
- #include <unistd.h>
- #include "wrap.h"
- #define MAXLINE 8192
- #define SERV_PORT 8000
- void do_sigchild(int num)
- {
- while (waitpid(0, NULL, WNOHANG)> 0)
- ;
- }
- int main(void)
- {
- struct sockaddr_in servaddr, cliaddr;
- socklen_t cliaddr_len;
- int listenfd, connfd;
- char buf[MAXLINE];
- char str[INET_ADDRSTRLEN];
- int i, n;
- pid_t pid;
- struct sigaction newact;
- newact.sa_handler = do_sigchild;
- sigemptyset(&newact.sa_mask);
- newact.sa_flags = 0;
- sigaction(SIGCHLD, &newact, NULL); // 建立信号, 处理子进程退出
- listenfd = Socket(AF_INET, SOCK_STREAM, 0);
- int opt = 1;
- setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口复用
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
- Listen(listenfd, 20);
- printf("Accepting connections ...\n");
- while (1) {
- cliaddr_len = sizeof(cliaddr);
- connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
- pid = fork();
- if (pid == 0) {
- Close(listenfd);
- while (1) {
- n = Read(connfd, buf, MAXLINE);
- if (n == 0) {
- printf("the other side has been closed.\n");
- break;
- }
- printf("received from %s at PORT %d\n",
- inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
- for (i = 0; i <n; i++)
- buf[i] = toupper(buf[i]);
- Write(STDOUT_FILENO, buf, n);
- Write(connfd, buf, n);
- }
- Close(connfd);
- return 0;
- } else if (pid> 0) {
- Close(connfd);
- } else
- perr_exit("fork");
- }
- return 0;
- }
- View Code
二, 线程实现并发服务器
理解了进程的方式, 就是创建多个线程来实现, 就不过多解释了, 程序需要对线程有一定了解, 之后我还会写篇博客来介绍线程, 敬请期待哦.
服务器代码如下, 有有详细的解释:
- #include <stdio.h>
- #include <string.h>
- #include <arpa/.NET.h>
- #include <pthread.h>
- #include <ctype.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include "wrap.h"
- #define MAXLINE 8192
- #define SERV_PORT 8000
- struct s_info { // 定义一个结构体, 将地址结构跟 cfd 捆绑
- struct sockaddr_in cliaddr;
- int connfd;
- };
- void *do_work(void *arg)
- {
- int n,i;
- struct s_info *ts = (struct s_info*)arg;
- char buf[MAXLINE];
- char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用 "[+d" 查看
- while (1) {
- n = Read(ts->connfd, buf, MAXLINE); // 读客户端
- if (n == 0) {
- printf("the client %d closed...\n", ts->connfd);
- break; // 跳出循环, 关闭 cfd
- }
- printf("received from %s at PORT %d\n",
- inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
- ntohs((*ts).cliaddr.sin_port)); // 打印客户端信息 (IP/PORT)
- for (i = 0; i <n; i++)
- buf[i] = toupper(buf[i]); // 小写 --> 大写
- Write(STDOUT_FILENO, buf, n); // 写出至屏幕
- Write(ts->connfd, buf, n); // 回写给客户端
- }
- Close(ts->connfd);
- return (void *)0;
- }
- int main(void)
- {
- struct sockaddr_in servaddr, cliaddr;
- socklen_t cliaddr_len;
- int listenfd, connfd;
- pthread_t tid;
- struct s_info ts[256]; // 根据最大线程数创建结构体数组.
- int i = 0;
- listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个 socket, 得到 lfd
- bzero(&servaddr, sizeof(servaddr)); // 地址结构清零
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定本地任意 IP
- servaddr.sin_port = htons(SERV_PORT); // 指定端口号 8000
- Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 绑定
- Listen(listenfd, 128); // 设置同一时刻链接服务器上限数
- printf("Accepting client connect ...\n");
- while (1) {
- cliaddr_len = sizeof(cliaddr);
- connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); // 阻塞监听客户端链接请求
- ts[i].cliaddr = cliaddr;
- ts[i].connfd = connfd;
- /* 达到线程最大数时, pthread_create 出错处理, 增加服务器稳定性 */
- pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
- pthread_detach(tid); // 子线程分离, 防止僵线程产生.
- i++;
- }
- return 0;
- }
- View Code
客户端代码如下:
- /* client.c */
- #include <stdio.h>
- #include <string.h>
- #include <unistd.h>
- #include <netinet/in.h>
- #include <arpa/.NET.h>
- #include "wrap.h"
- #define MAXLINE 80
- #define SERV_PORT 8000
- int main(int argc, char *argv[])
- {
- struct sockaddr_in servaddr;
- char buf[MAXLINE];
- int sockfd, n;
- sockfd = Socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr);
- servaddr.sin_port = htons(SERV_PORT);
- Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
- while (fgets(buf, MAXLINE, stdin) != NULL) {
- Write(sockfd, buf, strlen(buf));
- n = Read(sockfd, buf, MAXLINE);
- if (n == 0)
- printf("the other side has been closed.\n");
- else
- Write(STDOUT_FILENO, buf, n);
- }
- Close(sockfd);
- return 0;
- }
- View Code
三, select 实现并发服务器
select 和进程主要区别在于, 进程是阻塞的, 而 select 是交给内核自己来实现的, 由于 select 比较复杂
来源: https://www.cnblogs.com/liudw-0215/p/9664204.html