一、問題引入
UNIX網絡編程:socket實現(xiàn)client/server通信 隨筆簡單介紹了TCP Server服務單客戶端的socket通信,但是并未涉及多客戶端通信。
對于網絡編程肯定涉及到多客戶端通信和并發(fā)編程 (指在同時有大量的客戶鏈接到同一服務器),故本隨筆補充這部分知識。
而且并發(fā)并發(fā)編程涉及到多進程、多線程,其中 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);
}
// 注冊信號捕捉函數
signal(SIGCHLD, sighandler);
for (;;)
{
// 接受來自客戶端的信息
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);
}
}
close(listenfd);
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進程,發(fā)現(xiàn)TCP對應的套接字狀態(tài):TIME_WAIT
,一段時間后,TCP對應的套接字進程才消失。
2-4 程序解讀
- 客戶端
客戶端程序的主要功能是發(fā)送消息給服務器,并接受來自服務器的消息。
- 服務器
服務器程序的主要功能是:
1)接受多個客戶端的連接,并為每個客戶端派生子進程負責通信,父進程負責接受客戶端的連接
2)接受來自不同客戶端的消息,并將消息加工發(fā)送給對應的客戶端
- fork()
// pid_t 數據類型聲明在如下頭文件中
#include <sys/types.h>
// fork()原型聲明在如下頭文件中
#include <unistd.h>
/*
* fork() 通過復制調用進程來創(chuàng)建一個新進程,子進程是父進程的一個副本
* @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);
}
新的客戶端由子進程提供服務,同時關閉父進程的監(jiān)聽套接字listenfd
,父進程需要關閉已連接套接字connfd
? question
父進程對connfd 調用close 沒有終止它與客戶的連接呢?
為了便于理解,我們必須知道每個文件或套接字都有一個引用計數。引用計數在文件表項中維護(APUE第58~59頁),它是當前打開著的引用該文件或套接字的描述符的個數。socket 返回后與listenfd 關聯(lián)的文件表項的引用計數值為1。accept 返回后與connfd 關聯(lián)的文件表項的引用計數值也為1。然而fork 返回后,這兩個描述符就在父進程與子進程間共享(也就是被復制),因此與這兩個套接字相關聯(lián)的文件表項各自的訪問計數值均為2。這么一來,當父進程關閉connfd時,它只是把相應的引用計數值從2減為1,當子進程關閉listenfd時,它只是把相應的引用計數值從2減為1。該套接字真正的清理和資源釋放要等到其引用計數值到達0時才發(fā)生。
fork() 的兩個典型用法:
1)一個進程創(chuàng)建一個自身的副本,這樣每個副本都可以在另一個副本執(zhí)行其他任務的同時處理各自的某個操作。例如網絡服務器
2)一個進程創(chuàng)建一個自身的副本,然后其中一個副本(通常為子進程)調用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
表示:子進程狀態(tài)發(fā)生變化。通過signal函數,一旦服務器和客戶端通信異常,即可捕捉服務器中和客戶端通信的子進程。
三、反思總結
服務器與多客戶端通信,涉及到多進程的處理,子進程結束,如何監(jiān)控子進程的pid。
?? 問題思考:隨筆例子中,若服務器子進程結束,客戶端是無感知的,僅當客戶端往服務器再次發(fā)送數據才能得知服務器子進程結束。那么如何在服務器子進程一結束,客戶端進程立馬能知道呢?(待解決。。。)
四、參考引用
UNIX網絡編程 卷1:套接字聯(lián)網API 第3版文章來源:http://www.zghlxwxcb.cn/news/detail-448901.html
Unix網絡編程學習筆記文章來源地址http://www.zghlxwxcb.cn/news/detail-448901.html
到了這里,關于Linux網絡編程:socket & fork()多進程 實現(xiàn)clients/server通信的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!