多进程并发服务器浅谈

引入

上一篇博客我们只说了对于一个客户端进行处理数据,但是在实际开发中,我们肯定是并发的对多个客户端进行服务;
实现并发服务器的方式有很多种,我们今天讲讲最简单实现的,就是利用多进程进行实现;

思路分析

在上一篇博客中,我们提到Socket编程的基本流程图;

我们可以看到,在创建好套接字之后,服务器端就阻塞在accept函数上,等待客户端的链接;那么我们就可以在客户端建立连接之后创建一个子进程来处理这个客户端,而我们的父进程继续监听等待其他客户端来链接;
简单来说就是 每当有客户端要和服务器连接,那么我们就创建一个子进程来处理这个客户端; 所以我们不难写出代码;
注意事项
1.由于父子进程会共享一些局部变量(写时拷贝),所以我们需要在子进程中关闭负责监听的文件描述符,在父进程关闭连接到服务器的文件描述符;
2.由于子进程只有其父进程可以进行回收,所以这个顺序不能反过来;
即不可以子进程进行监听而父进程进行处理;

服务器端代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <stdlib.h>

#define IP "127.0.0.1"
#define PORT 6666

int main()
{
int lfd, cfd, i, n, opt;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
pid_t pid;
char buf[BUFSIZ], str[BUFSIZ]; // buf用来接收发送数据 str记录客户端ip
// 创建socket套接字 ipv4 TCP协议传输
lfd = socket(AF_INET, SOCK_STREAM, 0);

// server_addr 初始化
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT); // 本地字节序转为网络字节序
inet_pton(lfd, IP, &server_addr.sin_addr.s_addr);// 点分十进制转为网络字节序

//设置端口复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

//绑定 套接字与IP和端口
bind(lfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

//设置同时连接的上限
listen(lfd, 128);

while(1) // 循环等待连接
{
//阻塞等待客户端连接
client_len = sizeof(client_addr);
cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_len);

//有客户端进行连接 则创建一个进程进行处理
pid = fork();
if(pid == 0)//子进程 子进程进行处理客户端
{
close(lfd); // 关闭父进程用来监听的文件描述符 防止错误
// 打印客户端的IP和端口
printf("====IP:%s,PORT:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

//从套接字中读取数据
n = read(cfd, buf, sizeof(buf));
if(n <= 0) // 异常退出
exit(0);
printf("data is : %s\n", buf);//将数据输出一下
//对数据进行处理
for(i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
//写会客户端
write(cfd, buf, n);
exit(0); // 记得结束子进程 否则会死循环
}
else //父进程
{
close(cfd); // 关闭客户端新连接文件描述符
}
}

close(lfd);
close(cfd);

return 0;
}

复制代码并命名为fork.c
然后在终端输入 gcc fork.c -o fork -w

可以看到 就生成了一个可执行程序;

./fork 运行程序

运行程序之后,我们新开两个终端进行测试;
我们可以用nc命令进行连接

nc 127.0.0.1 6666

然后我们输入数据,可以发现客户端可以回复我们想要的数据;

对上面的代码一些问题进行解释;

setsockopt

这个函数是端口复用函数,为啥要用这个呢? 比如我们的程序是6666端口,然后我们进行测试的时候发现某个地方需要更改,当我们改完代码编译并运行,发现nc并不能连到当前服务器;

如果你了解过TCP的状态转换你可能就明白;简单来说就是TCP主动断开连接的一方需要等待2MLS的时间(Linux下一般为1min),在这段时间内这个端口是被占用的,所以我们的客户端连不到此服务器;而端口复用则可以解决这一个问题;

———好饿啊———吃完饭写———-

客户端端代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>

#define IP "127.0.0.1"
#define PORT 6666

int main()
{
int cfd, i, n;
struct sockaddr_in client_addr;
char buf[BUFSIZ], str[BUFSIZ];

// 创建套接字
cfd = socket(AF_INET, SOCK_STREAM, 0);

// 客户端不需要关心自己IP和端口 操作系统会自动分配 当然 也可以自己指定

// 套接字初始化
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(PORT);
inet_pton(cfd, IP, &client_addr.sin_addr.s_addr);

// 与服务器进行连接
connect(cfd, (struct sockaddr *)&client_addr, sizeof(client_addr));

while(1)
{
// 客户端通过键盘 写数据到buf
fgets(buf, sizeof(buf), stdin);

// 通过套接字写数据到服务器端
write(cfd, buf, strlen(buf));
//读取服务器写会的数据
n = read(cfd, buf, sizeof(buf));
printf("%s\n", buf);
}

close(cfd);

return 0;
}
-------------本文结束,感谢您的阅读!-------------