Linux网络编程:socket & fork()多进程 实现clients/server通信
一、问题引入
Linux网络编程:socket实现client/server通信 随笔简单介绍了TCP Server服务单客户端的socket通信,但是并未涉及多客户端通信。
对于网络编程肯定涉及到多客户端通信和并发编程 (指在同时有大量的客户链接到同一服务器),故本随笔补充这部分知识。
而且并发并发编程涉及到多进程、多线程,其中 fork()函数是Unix中派生新进程的唯一方法。
二、解决过程
2-1 server 代码
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define IP "10.8.198.227"
#define PORT 8887
static int string_toupper(const char *src, int str_len, char *dst)
{
int count = 0;
for (int i = 0; i < str_len; i++)
{
dst[i] = toupper(src[i]);
count++;
}
return count;
}
static int handle(int connect_fd, const char *socket)
{
int recv_len, send_len;
char read_buf[1024], send_buf[1024];
for (;;)
{
memset(read_buf, 0, sizeof(read_buf));
recv_len = read(connect_fd, read_buf, sizeof(read_buf));
if (recv_len < 0)
{
printf("read error \n");
break;
}
else if (recv_len == 0)
{
printf("%s close \n", socket);
break;
}
printf("%s:%s(%d Byte)\n", socket, read_buf, recv_len);
send_len = string_toupper(read_buf, strlen(read_buf), send_buf);
write(connect_fd, send_buf, send_len);
if (strcmp("exit", read_buf) == 0)
{
printf("%s close \n", socket);
break;
}
}
return 0;
}
static void sighandler(int signum)
{
pid_t pid;
while (1)
{
pid = waitpid(-1, NULL, WNOHANG);
if (pid > 0)
{
printf("child %d terminated\n", pid);
}
if (pid == -1 || pid == 0)
break;
}
}
int main(void)
{
int listenfd, connfd;
struct sockaddr_in server_sockaddr;
struct sockaddr_in client_addr;
char buf[1024];
char client_socket[128];
socklen_t length;
int pid;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = inet_addr(IP);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("socket error");
exit(1);
}
if (bind(listenfd, (struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr)) < 0)
{
perror("bind error");
exit(1);
}
if (listen(listenfd, 5) < 0)
{
perror("listen error");
exit(1);
}
for (;;)
{
// 注册信号捕捉函数
signal(SIGCHLD, sighandler);
// 接受来自客户端的信息
printf("accept start \n");
memset(&client_addr, 0, sizeof(client_addr));
length = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *)&client_addr, &length)) < 0)
{
if (errno == EINTR)
continue;
else
{
perror("accept error");
exit(1);
}
}
printf("client addr:%s por:%d\n",
inet_ntop(AF_INET, &client_addr.sin_addr, buf, sizeof(buf)),
ntohs(client_addr.sin_port));
snprintf(client_socket, sizeof(client_socket), "client socket (%s:%d)",
inet_ntop(AF_INET, &client_addr.sin_addr, buf, sizeof(buf)),
ntohs(client_addr.sin_port));
pid = fork();
if (pid == 0) // 子进程
{
close(listenfd);
handle(connfd, client_socket);
close(connfd);
exit(1);
}
else if (pid < 0) // error
{
perror("fork error");
close(connfd);
exit(1);
}
else // 父进程
{
close(connfd);
}
}
return EXIT_SUCCESS;
}
2-2 client 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define IP "10.8.198.227"
#define PORT 8887
static int handle(int connect_fd, const char *socket)
{
char send_buf[1024], recv_buf[1024];
int recv_len;
for (;;)
{
memset(send_buf, 0, sizeof(send_buf));
memset(recv_buf, 0, sizeof(recv_buf));
fgets(send_buf, sizeof(send_buf), stdin);
if (strlen(send_buf) <= 1)
continue;
if (send_buf[strlen(send_buf) - 1] == '\n')
send_buf[strlen(send_buf) - 1] = '\0';
write(connect_fd, send_buf, strlen(send_buf));
if (strcmp("exit", send_buf) == 0)
break;
recv_len = read(connect_fd, recv_buf, sizeof(recv_buf));
if (recv_len <= 0)
{
printf("read error or server closed, n==[%d] \n", recv_len);
break;
}
printf("%s:%s(%d Byte)\n", socket, recv_buf, recv_len);
}
return 0;
}
int main(void)
{
int sockfd;
char buf[1024];
struct sockaddr_in server_addr;
char server_socket[128];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(IP);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
printf("connect error \n");
return -1;
}
printf("server addr:%s por:%d\n",
inet_ntop(AF_INET, &server_addr.sin_addr, buf, sizeof(buf)),
ntohs(server_addr.sin_port));
snprintf(server_socket, sizeof(server_socket), "server socket (%s:%d)",
inet_ntop(AF_INET, &server_addr.sin_addr, buf, sizeof(buf)),
ntohs(server_addr.sin_port));
handle(sockfd, server_socket);
close(sockfd);
return EXIT_SUCCESS;
}
2-3 运行测试
1、client 1 连接 server
💡 注意:server后台查看TCP进程时,client的端口号可能不一致(原因是图片是后补的)
2、client 2 连接 server
💡 注意:server后台查看TCP进程时,client的端口号可能不一致(原因是图片是后补的)
3、多客户端与服务器通信
4、client 2断开与 server的连接
client 2后台查看TCP进程,发现TCP对应的套接字状态:TIME_WAIT
,一段时间后,TCP对应的套接字进程才消失。
2-4 程序解读
- 客户端
客户端程序的主要功能是发送消息给服务器,并接受来自服务器的消息。
- 服务器
服务器程序的主要功能是:
1)接受多个客户端的连接,并为每个客户端派生子进程负责通信,父进程负责接受客户端的连接
2)接受来自不同客户端的消息,并将消息加工发送给对应的客户端
- fork()
// pid_t 数据类型声明在如下头文件中
#include <sys/types.h>
// fork()原型声明在如下头文件中
#include <unistd.h>
/*
* fork() 通过复制调用进程来创建一个新进程,子进程是父进程的一个副本
* @return 若成功,返回值有效范围:0~32767;否则失败,返回值-1
*/
pid_t fork(void);
👉 任何子进程只有一个父进程,而且子进程总是可以通过 getppid()
获取父进程的 pid。但是一个父进程可以拥有n (n >= 0) 个子进程
fork()
调用一次,它却返回两次。它在调用进程(即父进程)中返回一次,返回值是新派生进程(即子进程)的pid;在子进程中又返回一次,返回值是0。
代码中可以看到:
pid = fork();
if (pid == 0) // 子进程
{
close(listenfd);
handle(connfd, client_socket);
exit(1);
}
else if (pid < 0) // error
{
perror("fork error");
close(connfd);
exit(1);
}
else // 父进程
{
close(connfd);
}
新的客户端由子进程提供服务,同时关闭父进程的监听套接字listenfd
,父进程需要关闭已连接套接字connfd
❓ question
父进程对connfd 调用close 没有终止它与客户的连接呢?
为了便于理解,我们必须知道每个文件或套接字都有一个引用计数。引用计数在文件表项中维护(APUE第58~59页),它是当前打开着的引用该文件或套接字的描述符的个数。socket 返回后与listenfd 关联的文件表项的引用计数值为1。accept 返回后与connfd 关联的文件表项的引用计数值也为1。然而fork 返回后,这两个描述符就在父进程与子进程间共享(也就是被复制),因此与这两个套接字相关联的文件表项各自的访问计数值均为2。这么一来,当父进程关闭connfd时,它只是把相应的引用计数值从2减为1,当子进程关闭listenfd时,它只是把相应的引用计数值从2减为1。该套接字真正的清理和资源释放要等到其引用计数值到达0时才发生。
fork() 的两个典型用法:
1)一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。例如网络服务器
2)一个进程创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec() 把自身替换为新的程序。例如shell程序
- signal()
// signal()原型声明在如下头文件中
#include <signal.h>
typedef void (*sighandler_t)(int);
/*
* @param signum 信号,可以根据对应信号,函数指针对信号进行处理
* @param handler 函数指针
* @return 返回值是一个函数指针
*/
sighandler_t signal(int signum, sighandler_t handler);
信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分
随笔中:signal(SIGCHLD, sighandler);
,其中 SIGCHLD
表示:子进程状态发生变化。通过signal函数,一旦服务器和客户端通信异常,即可捕捉服务器中和客户端通信的子进程。
三、反思总结
服务器与多客户端通信,涉及到多进程的处理,子进程结束,如何监控子进程的pid。
四、参考引用
UNIX网络编程 卷1:套接字联网API 第3版