socket服务器编程:
顾名思义就是使用socket套接字来编写服务器程序的过程。不熟悉socket编程的小伙伴可以看我之前的文章,但是当时所实现的功能服务器同时只能和一个客户端进行交互,效率太低,利用多进程或者多线程方式来实现服务器可以做到同时和多个客户端进行交互。提高服务器的性能。
多进程的实现方式:
我们可以在程序中使用fork系统调用来创建一个子进程,父进程负责接收客户端的连接,并从监听队列中取出已经连接成功的一个客户端套接字,传给子进程,子进程来负责处理和唯一的一个客户端的交互。当我们将上面这个过程实现在一个while循环中时,父进程循环接收,循环创建子进程,每个子进程都可以处理一个客户端的请求,这样我们就可以同时和多个客户端进行交互。
原理:这里我们使用了fork系统调用的一个性质------->父子进程会共享fork之前创建的文件描述符。子进程创建出来之后,会将父进程的PCB结构复制一份,文件描述符也就会被复制过来,而父子进程的两个文件描述符,指向了同一个struct_file的结构体,在这个结构体中,有一个count属性,专门用来记录指向该结构体的文件描述符个数。
当count的值是0时,该结构体被释放,子进程创建出来后该属性的值从1变成2(父进程+子进程),但是我们要将父进程中的文件描述符关闭,因为我们指定子进程来处理和客户端的交互,当完成交互过程的时候,该结构体理应被释放,也就是count应该由1减为0,如果我们不关闭父进程中的文件描述符,完成交互时,只会将count减一,并不会释放该结构体。导致每有一个客户端连接服务器,父进程就会多出一个未被释放的文件描述符,当我们有20个客户端建立过连接后其他客户端将无法再和该服务器进行连接(一个进程最多同时打开20个文件,一个系统最多同时打开64个struct_file结构体)就会造成资源极大极大的浪费。
还要注意的是由于我们父进程是一直循环接收客户端的连接,而子进程只负责和一个客户端的交互,当一次交互完成后,一个子进程也就该结束掉了,当父进程未结束时,已经结束的子进程就会变成僵死进程,如果我们不采用手段来手动清除这些僵死进程,就会造成资源的极大浪费。这里我们使用信号响应机制来处理僵死子进程。
多线程的实现方式:
和多进程方式相似,多线程方式就是使用创建出来的函数线程进行和一个特定客户端的交互,而主线程负责接收所有客户端的连接。使用多线程的方式不能在主线程中关闭文件描述符,因为在同一个进程中,除了栈区的数据其他静态或者全局的数据都是共享的,也就是说,函数线程和主线程其实使用的是同一个文件描述符,如果主线程将其关闭,函数线程也无法使用。
需要注意的是:在将文件描述符传递给函数线程时,不能使用地址传递的方式,因为主线程和函数线程使用的是同一个文件描述符,而主线程需要不断的接收连接,有可能我们主线程将文件描述符的地址传递给函数线程时刚好接受了一个新的连接,而此时函数线程中从该地址读取到的文件描述符就是这个新连接到的值,上一个客户端的请求就会被遗漏,这时必须注意的一点。
多进程,多线程的异同:
- 编程角度:多线程比多进程要简单一些,不用做太多的控制
- 占用资源角度:多线程比多进程节省了更多的资源
- 主线程(父进程),函数线程(子进程)之间的切换:线程切换的效率高,进程相对较慢
- 数据共享方面:线程除栈区数据之外,静态或者全局的数据全部共享,进程几乎不共享数据
- 连接数量:进程1024个(一个系统最多同时维护1024个PCB结构),线程20个(一个进程最多同时打开20个文件描述符)
完整源代码:
- 多进程:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<assert.h> #include<sys/types.h> #include<sys/socket.h> #include<sys/un.h> #include<netinet/in.h> #include<arpa/inet.h> #include<pthread.h> #include<signal.h> /*子进程用来处理交互过程的函数*/ void func(int c) { while(1) { char buff[128] = {0}; int err = recv(c, buff, 127, 0); if(err <= 0) { printf("%dclient disconnect!\n",c); break; } printf("%d: %s\n",c,buff); char sendbuff[128] = {0}; printf("please input:"); fflush(stdout); fgets(sendbuff, 127, stdin); err = send(c, sendbuff, strlen(sendbuff)-1, 0); if(err == -1) { printf("error\n"); } } close(c); } /*避免僵死子进程,信号响应函数*/ void Zombie(int sign) { wait(NULL); } int main() { signal(SIGCHLD, Zombie);/*改变信号的响应方式*/ int sockfd = socket(PF_INET, SOCK_STREAM, 0); assert(sockfd != -1); struct sockaddr_in ser,cli; ser.sin_family = AF_INET; ser.sin_port = htons(6500); ser.sin_addr.s_addr = inet_addr("127.0.0.1"); int err = bind(sockfd, (struct sockaddr*)&ser, sizeof(ser)); assert(err != -1); err = listen(sockfd, 5); assert(err != -1); while(1) { int len = sizeof(cli); int c = accept(sockfd, (struct sockaddr*)&cli, &len); if(c == -1) { printf("accept error\n"); continue; } int n = fork(); if(n == 0) { func(c); break; } else { close(c);//父进程中关闭文件描述符 } } close(sockfd); return 0; }
- 多线程:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<assert.h> #include<sys/types.h> #include<sys/socket.h> #include<sys/un.h> #include<netinet/in.h> #include<arpa/inet.h> #include<pthread.h> /*函数线程*/ void *func(void* arg) { int c = (int)arg; while(1) { char buff[128] = {0}; int err = recv(c, buff, 127, 0); if(err <= 0) { printf("%dclient disconnect!\n",c); break; } printf("%d: %s\n",c,buff); err = send(c, "ok", 2, 0); if(err == -1) { printf("error\n"); } } close(c); } int main() { int sockfd = socket(PF_INET, SOCK_STREAM, 0); assert(sockfd != -1); struct sockaddr_in ser,cli; ser.sin_family = AF_INET; ser.sin_port = htons(6500); ser.sin_addr.s_addr = inet_addr("127.0.0.1"); int err = bind(sockfd, (struct sockaddr*)&ser, sizeof(ser)); assert(err != -1); err = listen(sockfd, 5); assert(err != -1); while(1) { int len = sizeof(cli); int c = accept(sockfd, (struct sockaddr*)&cli, &len); if(c == -1) { printf("accept error\n"); continue; } pthread_t id; int res = pthread_create(&id, NULL, func, (void*)c);/*采用值传递的方式传递文件描述符*/ assert(res == 0); } close(sockfd); return 0; }