前言:刚开始学网络编程,都会先写一个客户端和服务端,不知道你们有没有试一下:再打开一下客户端,是连不上服务端的。还有一个问题不知道你们发现没:有时启服务器,会提示“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/inet.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(, NULL, WNOHANG) > )
;
} 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 = ;
sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, ); // 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, ); printf("Accepting connections ...\n");
while () {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
pid = fork();
if (pid == ) {
Close(listenfd);
while () {
n = Read(connfd, buf, MAXLINE);
if (n == ) {
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 = ; i < n; i++)
buf[i] = toupper(buf[i]);
Write(STDOUT_FILENO, buf, n);
Write(connfd, buf, n);
}
Close(connfd);
return ;
} else if (pid > ) {
Close(connfd);
} else
perr_exit("fork");
}
return ;
}

  客户端程序如下:

  

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.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, ); 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 == ) {
printf("the other side has been closed.\n");
break;
}
else
Write(STDOUT_FILENO, buf, n);
} Close(sockfd); return ;
}

  演示效果,服务器可以同时处理两个客户端,如下:

  

  但我想再测试一下程序,执行./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/inet.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(, NULL, WNOHANG) > )
;
} 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 = ;
sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, ); int opt = ;
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, ); printf("Accepting connections ...\n");
while () {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
pid = fork();
if (pid == ) {
Close(listenfd);
while () {
n = Read(connfd, buf, MAXLINE);
if (n == ) {
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 = ; i < n; i++)
buf[i] = toupper(buf[i]);
Write(STDOUT_FILENO, buf, n);
Write(connfd, buf, n);
}
Close(connfd);
return ;
} else if (pid > ) {
Close(connfd);
} else
perr_exit("fork");
}
return ;
}

  二、线程实现并发服务器

  理解了进程的方式,就是创建多个线程来实现,就不过多解释了,程序需要对线程有一定了解,之后我还会写篇博客来介绍线程,敬请期待哦。

  服务器代码如下,有有详细的解释:

  

#include <stdio.h>
#include <string.h>
#include <arpa/inet.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 () {
n = Read(ts->connfd, buf, MAXLINE); //读客户端
if (n == ) {
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 = ; i < n; i++)
buf[i] = toupper(buf[i]); //小写-->大写 Write(STDOUT_FILENO, buf, n); //写出至屏幕
Write(ts->connfd, buf, n); //回写给客户端
}
Close(ts->connfd); return (void *);
} int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
pthread_t tid;
struct s_info ts[]; //根据最大线程数创建结构体数组.
int i = ; listenfd = Socket(AF_INET, SOCK_STREAM, ); //创建一个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, ); //设置同一时刻链接服务器上限数 printf("Accepting client connect ...\n"); while () {
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 ;
}

  客户端代码如下:

  

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.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, ); 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 == )
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
} Close(sockfd); return ;
}

  三、select实现并发服务器

  select和进程主要区别在于,进程是阻塞的,而select是交给内核自己来实现的,由于select比较复杂,参考我的另一篇博客:https://www.cnblogs.com/liudw-0215/p/9661583.html

  总结:有不懂的,欢迎及时评论。

  

  

  

并发服务器三种实现方式之进程、线程和select的更多相关文章

  1. MyEclipse中web服务器的三种配置方式

    初学Javaweb开发的人们都会遇到一个问题,就是服务器环境的搭建配置问题.下面介绍三种服务器的搭建方式. 直接修改server.xml文件 当你写了一个web应用程序(jsp/servlet),想通 ...

  2. 并发编程系列小结(线程安全,synchronized,脏读,线程间的通信wait/notify,线程的三种实现方式Demo,可替代wait/notify的方法)

    线程安全: 当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法就是线程安全的) synchronized: 可以在任意对象或方法上加锁,而加锁的这段代码称为 ...

  3. 三种存储方式DAS、NAS、SAN

    ------------恢复内容开始------------ 一.DAS.NAS.SAN在存储领域的位置 随着主机.磁盘.网络等技术的发展,数据存储的方式和架构也在一直不停改变,本文主要介绍目前主流的 ...

  4. IIS下PHP的三种配置方式比较

    在Windows IIS 6.0下配置PHP,通常有CGI.ISAPI和FastCGI三种配置方式,这三种模式都可以在IIS 6.0下成功运行,下面我就讲一下这三种方式配置的区别和性能上的差异. 1. ...

  5. Linq to Sql : 三种事务处理方式

    原文:Linq to Sql : 三种事务处理方式 Linq to SQL支持三种事务处理模型:显式本地事务.显式可分发事务.隐式事务.(from  MSDN: 事务 (LINQ to SQL)).M ...

  6. Java之线程安全中的三种同步方式

    一个程序在运行起来时,会转换为进程,通常含有多个线程. 通常情况下,一个进程中的比较耗时的操作(如长循环.文件上传下载.网络资源获取等),往往会采用多线程来解决. 比如,现实生活中,银行取钱问题.火车 ...

  7. [转]Apache HTTP Server 与 Tomcat 的三种连接方式介绍

    首先我们先介绍一下为什么要让 Apache 与 Tomcat 之间进行连接.事实上 Tomcat 本身已经提供了 HTTP 服务,该服务默认的端口是 8080,装好 tomcat 后通过 8080 端 ...

  8. 分布式锁的三种实现方式 数据库、redis、zookeeper

    版权声明: https://blog.csdn.net/wuzhiwei549/article/details/80692278 一.为什么要使用分布式锁 我们在开发应用的时候,如果需要对某一个共享变 ...

  9. 【重学Java】多线程基础(三种创建方式,线程安全,生产者消费者)

    实现多线程 简单了解多线程[理解] 是指从软件或者硬件上实现多个线程并发执行的技术. 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能. 并发和并行[理解] 并行:在同一时刻, ...

随机推荐

  1. 适用于 iOS、Android 和 Windows 设备的移动设备管理

    适用于 iOS.Android 和 Windows 设备的移动设备管理 随着企业环境中移动设备的数量不断增加,详细检查访问您企业资源的移动设备变得至关重要.统一终端管理软件 Desktop Centr ...

  2. thinkphp5省市区三级联动例子

    数据库 数据库下载地址:https://files.cnblogs.com/files/fan-bk/packet_region.zip php <?php namespace app\inde ...

  3. Codeforces Educational Codeforces Round 44 (Rated for Div. 2) E. Pencils and Boxes

    Codeforces Educational Codeforces Round 44 (Rated for Div. 2) E. Pencils and Boxes 题目连接: http://code ...

  4. <<君主论>>读后感

    “<君主论>与<圣经>齐名,被称为邪恶的圣经,这本书的立论基础是人性本恶论,他所描述的人性之恶,主要是以西欧资本原始积累背景下的现实社会中人的各种丑恶现象为蓝本的.” 要是高中 ...

  5. vs 2017 打开 iis express问题

    问题: 更新vs2017 15.6.4后,首次打开网站 iis express 一直报 无法连接到web服务器. 解决办法: 关闭防火墙,在次启动即可,启动成功后,在次打开防火墙也无影响.

  6. OC基础成员变量、属性变量、实例方法与类方法

    首先,为什么我们要定义一个新类呢?按照我的理解,就是为了抽象出来一个新的东西(也就是类),用来存储更多的数据变量和方法,一切类都直接或间接继承与NSObject. 在类的头文件里我们可以定义成员变量. ...

  7. [转] Java 的泛型擦除和运行时泛型信息获取

    原文链接 https://my.oschina.net/lifany/blog/875769 前言 现在很多程序员都会在简历中写上精通 Java.但究竟怎样才算是精通 Java 呢?我觉得不仅要熟练掌 ...

  8. 推导正交投影(Orthographic Projection)

    定义六个面 left right bottom top near far   然后三个轴分开考虑 x轴 视椎体的x范围在[l,r],我们要变换到[-1,1] 1 减去l变换到[0, r-l] 2 乘以 ...

  9. AFNetWorking使用自签证书验证

    In order to validate a domain name for self signed certificates, you MUST use pinning 上述问题的解决方法: sec ...

  10. python jieba库的基本使用

    第一步:先安装jieba库 输入命令:pip install jieba jieba库常用函数: jieba库分词的三种模式: 1.精准模式:把文本精准地分开,不存在冗余 2.全模式:把文中所有可能的 ...