上一节,我们实现了一个客户端/服务端的Socket通信的代码,在这个例子中,客户端连接上服务端后发送一个字符串,而服务端接收到字符串并打印出来后就关闭所有套接字并退出了。
上一节的代码较为简单,在实际的应用中,客户端和服务端需要像一个聊天室一样能够收发信息,但这样就引出了一些问题:
1、服务端程序需要既能accept
新的客户端请求,又能实时获与已经建立连接的客户端发来的消息
2、客户端程序需要既能从stdin
获取用户输入,又能实时获取从服务端发来的消息
下面就来解决这两个问题。
文章目录
- 1 代码改进
- 1.1 fork函数
- 1.2 服务端代码
- 1.3 客户端代码
- 1.4 实验结果
- 2 代码优化:信号机制之kill和signal函数
- 2.1 kill函数
- 2.2 signal函数
- 2.3 客户端代码改进
- 2.4 实验结果
- 3 完整代码
- 3.1 服务端
- 3.2 客户端
1 代码改进
以服务端的问题为例,在accept
了一个客户端之后,就在循环中无限检测这个套接字是否有新的数据到来。此时如果又有一个客户端想要进行连接,由于服务端没有在accept
,那么就无法建立连接。在这里介绍其中一种解决方法:fork
函数。
1.1 fork函数
fork
函数是Linux系统中用于创建新进程的系统调用。它在调用进程内部创建一个与父进程几乎完全相同的子进程。这个子进程是父进程的副本,包括代码段、数据段、堆栈等,但具有不同的进程ID(PID
)。
pid_t fork(void);
返回值:fork
函数在父进程中返回两次,在父进程中返回的值为子进程的PID,而在子进程中返回的值为0。若系统资源不足,则返回-1。
例子:创建一个父进程,然后使用fork
函数创建了一个子进程,分别输出它们的PID。父进程和子进程可以并发执行,并且各自具有不同的PID。
#include <stdio.h>
#include <unistd.h>int main() {pid_t child_pid = fork();if (child_pid == -1) {perror("fork");return 1;}if (child_pid == 0) {// This is the child processprintf("Child process, PID: %d\n", getpid());} else {// This is the parent processprintf("Parent process, PID: %d, Child PID: %d\n", getpid(), child_pid);}return 0;
}
运行结果:
可以看到if
的两个分支并发执行,并输出PID。
1.2 服务端代码
现在就将accept
放入while循环中,来监听多个客户端的请求,对于不同客户端fork
一个子进程对数据进行处理,更改代码如下:
其中socket_read
函数如下:
- 这里服务端在收到客户端消息后,固定回复
server gets message successfully
这里有几个点需要注意:
(1)在父进程中关闭子进程的socket,在子进程中关闭父进程的socket?
关闭套接字只会影响当前进程的套接字,不会直接影响其他进程的套接字,包括父进程的套接字。这是因为每个进程都有其独立的文件描述符表。在子进程中关闭套接字后,父进程的套接字仍然保持打开状态,不受影响。
(2)recv()
函数
- 返回0时,一般表示客户端断开了连接
- 该函数不会在结尾添加
\0
,需要自行添加
1.3 客户端代码
客户端现在需要实现一遍通过stdin
输入数据并发给服务端,一边接收服务端的数据。同样地,这也可以使用fork
来解决:
这里在子进程中接收服务端的消息,在父进程中接收标准输入的数据并发送给服务端。
1.4 实验结果
2 代码优化:信号机制之kill和signal函数
在前面的代码中,send
最好和recv
一样判断返回值,在返回0的时候表示对端断开了连接,此时要关闭套接字。
现在以客户端为例再来理一下这里面的逻辑,在客户端中,子进程用来接收服务端的消息,父进程用来接收标准输入。如果服务端断开了连接,客户端的子进程的recv
将返回0,从而退出;但是从逻辑上来看,服务端都退出了,此时父进程监听stdin
也没有什么意义了。
**所以我们需要在客户端中实现:在子进程退出的同时,主动关闭父进程。**我们可以利用Linux中的信号机制来实现这个功能,这里简单地介绍一下kill
和signal
函数的使用:
2.1 kill函数
kill
是一个用于发送信号给指定进程的系统调用。它的原型如下:
int kill(pid_t pid, int sig);
pid
参数指定目标进程的PID。可以使用不同的PID值来选择不同的目标:正整数表示具体的进程,0表示向与调用进程属于同一个进程组的所有进程发送信号,-1表示向所有具有权限的进程发送信号,-2表示向与调用进程属于同一个会话的所有进程发送信号。sig
参数是要发送的信号的编号- 通常我们可以使用常见的信号宏(如
SIGTERM
、SIGKILL
、SIGINT
等)作为参数,这些特殊的信号宏的回调函数由内核实现。比如进程接收到SIGKILL
信号时,它会被立即终止。 - 我们也可以使用一些自定义的宏(如
SIGUSR1
),然后自定义回调函数
- 通常我们可以使用常见的信号宏(如
2.2 signal函数
signal
函数用于注册信号处理函数,即在收到特定信号时执行的用户定义的函数。它的原型如下:
void (*signal(int signum, void (*handler)(int)))(int);
signum
参数是要处理的信号的编号。handler
参数是一个函数指针,指向信号处理函数。通常,你可以使用一个函数来处理特定信号,或者使用SIG_IGN
表示忽略信号,使用SIG_DFL
表示使用默认的信号处理方式。
2.3 客户端代码改进
我们需要实现在子进程退出的同时,主动关闭父进程:
1、在父进程代码开始处注册信号处理函数
其中handler
函数如下:
也就是说,在父进程收到SIGUSR1
信号的时候,会调用exit
退出。
2、在子进程退出时调用kill
函数向父进程发送SIGUSR1
信号
- 这里的
getppid()
用于获取父进程的pid
当然这里只是举一个例子,实际上可以不用在父进程中注册信号和回调函数,直接使用内核提供的SIGKILL
信号,在子进程最后调用kill(getppid(), SIGUSR1)
,效果也是一样的。
2.4 实验结果
3 完整代码
3.1 服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>void socket_read(int s, struct sockaddr_in *addr)
{char buffer[1024];while (1){ssize_t bytes_received = recv(s, buffer, sizeof(buffer), 0);if (bytes_received == -1) {perror("Receive failed");} else if(bytes_received == 0){printf("peer closed\n");break;} else {buffer[bytes_received] = '\0';printf("recv from %s:%d = %s\n",inet_ntoa(addr->sin_addr), ntohs(addr->sin_port), buffer);send(s, "server gets message successfully\n", strlen("server gets message successfully\n"), 0);}}
}int main() {// 创建套接字int server_socket = socket(AF_INET, SOCK_STREAM, 0);if (server_socket == -1) {perror("Socket creation failed");exit(1);}int reuse = 1;setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));// 绑定套接字到IP地址和端口struct sockaddr_in server_address;server_address.sin_family = AF_INET;server_address.sin_port = htons(8080);server_address.sin_addr.s_addr = INADDR_ANY;if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {perror("Bind failed");close(server_socket);exit(1);}// 设置服务器套接字为监听状态if (listen(server_socket, 5) == -1) {perror("Listen failed");close(server_socket);exit(1);}printf("Server listening on port 8080...\n");// 接受客户端连接struct sockaddr_in client_address;socklen_t client_len = sizeof(client_address);pid_t child_pid;while(1){int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_len);if (client_socket == -1) {perror("Accept failed");close(server_socket);exit(1);}// 打印客户端的IP和端口号printf("accept new:ip=%s port=%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));// 创建子进程child_pid = fork();if (child_pid == -1) {perror("fork");return 1;}if (child_pid == 0) {// 在子进程中可以关闭掉父进程的套接字close(server_socket);// 数据接收函数,在while中循环读socket是否有数据并输出socket_read(client_socket, &client_address);// 如何该子进程对应的客户端退出了,该函数返回,应该退出子进程,防止子进程acceptexit(1);} else {// 在父进程中可以关闭掉子进程的套接字,继续下一次while循环执行acceptclose(client_socket);}}return 0;
}
3.2 客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>void handler(int sig)
{printf("recv a sig=%d\n", sig);exit(1);
}int main() {// 创建套接字int client_socket = socket(AF_INET, SOCK_STREAM, 0);if (client_socket == -1) {perror("Socket creation failed");exit(1);}// 设置服务器地址和端口struct sockaddr_in server_address;server_address.sin_family = AF_INET;server_address.sin_port = htons(8080);server_address.sin_addr.s_addr = inet_addr("127.0.0.1");// 连接到服务器if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {perror("Connection failed");close(client_socket);exit(1);}// 创建子进程pid_t child_pid;child_pid = fork();if (child_pid == -1) {perror("fork");return 1;}if (child_pid == 0) {// 子进程用来接收服务端发来的消息char recv_buf[1024];while(1){ssize_t bytes_received = recv(client_socket, recv_buf, sizeof(recv_buf), 0);if (bytes_received == -1) {perror("Receive failed");} else if(bytes_received == 0){printf("peer closed\n");break;} else{recv_buf[bytes_received] = '\0';printf("recv %ld bytes from server:%s\n", bytes_received, recv_buf);}}close(client_socket);kill(getppid(), SIGUSR1);} else {// 父进程用来从stdin中接收数据signal(SIGUSR1, handler);char stdin_buf[1024];while(fgets(stdin_buf, sizeof(stdin_buf), stdin) != NULL){send(client_socket, stdin_buf, strlen(stdin_buf), 0);}close(client_socket);}return 0;
}