此为牛客Linux C++课程和黑马Linux系统编程笔记。
1. 多进程版
1.1 思路
大体思路与上一篇的单进程版服务器–客户端类似,都是遵循下图:
多进程版本有以下几点需要注意:
- 由于TCP是点对点连接,服务器主进程连接了一个客户端以后就无法再与其他客户端相连,所以多进程版的服务器中的父进程只负责监听,连接并信息传输的工作交给子进程完成。每当accept到一个客户端的连接请求,就fork出一个子进程来处理。
- 父进程负责监听的同时,也要回收子进程的资源,避免产生僵尸进程。
1.2 服务端
/*实现一个简单的多进程服务器-客户端通信*/#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>// 设定一个服务器端口号
#define SERV_IP "127.0.0.1"
#define SERV_PORT 7777void wait_child(int signo) {while(1) {waitpid(-1, NULL, WNOHANG); // 回收任意子进程并设置成非阻塞}return;
}int main()
{int lfd, cfd; // 用于监听的文件描述符和用于与客户端通信的文件描述符struct sockaddr_in serv_addr, clie_addr; // 服务器和客户端的sockaddrlfd = socket(AF_INET, SOCK_STREAM, 0); // 服务端套接字serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT); // 注意转化成网络字节序inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 注意转化成网络字节序bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 与ip和端口号绑定listen(lfd, 128); // 指定最多同时连接数128int pid;while(1) {// 父进程循环进行acceptint clie_addr_len = sizeof(clie_addr);cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);char clie_IP[BUFSIZ];printf("Client IP: %s, client port: %d connected\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),ntohs(clie_addr.sin_port));pid = fork();if(pid > 0) {// 父进程close(cfd); // 父进程只需要循环监听,不需要与客户端进行数据交互,故关闭signal(SIGCHLD, wait_child); // 回收子进程,避免产生僵尸进程,也可以用sigaction} else if(pid == 0) {// 子进程,注意这里的写法,因为子进程不需要循环,所以把子进程的逻辑定义在循环外部,在这里break出去close(lfd); // 子进程不需要监听,所以把子进程的lfd关掉break;} else {perror("fork error");exit(1);}}if(pid == 0) {// 子进程负责跟一个客户端完成数据交互while(1) {char buf[BUFSIZ];int len;len = read(cfd, buf, sizeof(buf));if(len > 0) {// 小写转大写int i;for(i = 0; i < len; ++i) {if(buf[i] >= 'a' && buf[i] <= 'z') {buf[i] -= 32;}}write(cfd, buf, len); // 写回给客户端write(STDOUT_FILENO, buf, len);} else if(len == 0){// ret为0说明读完了,表示客户端已关闭close(cfd);exit(1);} else {perror("read error");exit(1);}}}return 0;
}
为突出主体,未写错误检测与错误提示
1.3 客户端
/*实现一个简单的多进程服务器-客户端通信*/#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>// 服务器的ip和端口
#define SERV_IP "127.0.0.1"
#define SERV_PORT 7777int main()
{int ret; // 用于错误检测int cfd; // 用于写入数据传输给服务端的socket的文件描述符cfd = socket(AF_INET, SOCK_STREAM, 0);if(cfd == -1) {perror("socket error");exit(1);}// bind() 可以不调用bind(), linux会隐式地绑定struct sockaddr_in serv_addr; // 因为要连接服务端,这里的sockadd_in是用于指定服务端的ip和端口bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 调用ip转换函数,把字符串ip转化为网络字节序ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1) {perror("connect error");exit(1);}// 从终端读取内容while(1) {char buf[BUFSIZ];fgets(buf, sizeof(buf), stdin); // 读一行// 写入到cfd中,传输给服务端ret = write(cfd, buf, strlen(buf)); // 注意不要写成sizeof(buf),sizeof是在内存中所占的大小,strlen是到第一个'\0'位止。if(ret == -1) {perror("write error");exit(1);}// read在读socket时默认时阻塞的,阻塞等待服务端传输数据int len;len = read(cfd, buf, sizeof(buf));if(len == -1) {perror("read error");exit(1);}printf("%s", buf);}close(cfd);return 0;
}
为突出主体,未写错误检测与错误提示
2. 多线程版本
使用与多进程完全类似的思路,无非是用线程来实现。
2.1 服务端
/*实现一个简单的多线程服务器-客户端通信*/#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <pthread.h>
#include <strings.h>// 设定一个服务器端口号
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888struct s_info /* 该结构体用于给子线程函数传参 */
{struct sockaddr_in clie_addr; // 客户端ip和端口号int cfd; // 通信所用的文件描述符
};void* do_work(void* arg) {// 子线程负责小写转大写struct s_info *ts = (struct s_info*)arg;while(1) {char buf[BUFSIZ];int len;len = read(ts->cfd, buf, sizeof(buf));if(len > 0) {// 小写转大写int i;for(i = 0; i < len; ++i) {if(buf[i] >= 'a' && buf[i] <= 'z') {buf[i] -= 32;}}write(ts->cfd, buf, len); // 写回给客户端write(STDOUT_FILENO, buf, len);} else if(len == 0){// ret为0说明读完了,表示客户端已关闭close(ts->cfd);exit(1);} else {perror("read error");exit(1);}}return (void*)0;
}int main()
{int lfd, cfd; // 用于监听的文件描述符和用于与客户端通信的文件描述符struct sockaddr_in serv_addr, clie_addr; // 服务器和客户端的sockaddrlfd = socket(AF_INET, SOCK_STREAM, 0); // 服务端套接字serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT); // 注意转化成网络字节序inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 注意转化成网络字节序bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 与ip和端口号绑定listen(lfd, 128); // 指定最多同时连接数128struct s_info ts;pthread_t tid;while(1) {// 主线程循环进行acceptint clie_addr_len = sizeof(clie_addr);cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);char clie_IP[BUFSIZ];printf("Client IP: %s, client port: %d connected\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),ntohs(clie_addr.sin_port));bzero(&ts, sizeof(ts));ts.cfd = cfd;ts.clie_addr = clie_addr;pthread_create(&tid, NULL, do_work, (void*)&ts);pthread_detach(tid); // 子线程分离,防止产生僵尸线程}printf("what happened?");return 0;
}
2.2 客户端
/*实现一个简单的多线程服务器-客户端通信*/#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>// 服务器的ip和端口
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888int main()
{int ret; // 用于错误检测int cfd; // 用于写入数据传输给服务端的socket的文件描述符cfd = socket(AF_INET, SOCK_STREAM, 0);if(cfd == -1) {perror("socket error");exit(1);}// bind() 可以不调用bind(), linux会隐式地绑定struct sockaddr_in serv_addr; // 因为要连接服务端,这里的sockadd_in是用于指定服务端的ip和端口bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 调用ip转换函数,把字符串ip转化为网络字节序ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1) {perror("connect error");exit(1);}// 从终端读取内容while(1) {char buf[BUFSIZ];fgets(buf, sizeof(buf), stdin); // 读一行// 写入到cfd中,传输给服务端ret = write(cfd, buf, strlen(buf)); // 注意不要写成sizeof(buf),sizeof是在内存中所占的大小,strlen是到第一个'\0'位止。if(ret == -1) {perror("write error");exit(1);}// read在读socket时默认时阻塞的,阻塞等待服务端传输数据int len;len = read(cfd, buf, sizeof(buf));if(len == -1) {perror("read error");exit(1);}printf("%s", buf);}close(cfd);return 0;
}