前文我们实现了一个 socket 最小的实现,它只允许一台终端连接到服务器进行数据通信,但这样的程序对我们来说没有什么意义,所以我们一定要实现多个客户端与一个服务端通信交互数据,这样才能真正派上用场,所以本文主要介绍了两种实现多客户端连接的方案,一种是多进程,一种是多线程,两种性能相差无几,但明显多线程在资源方面明显要比多进程消耗要少的多。
公共头文件
/* wrap.h */ #ifndef __WRAP_H__ #define __WRAP_H__ void perr_exit(const char* s); int Accept(int fd, struct sockaddr* sa, socklen_t* salenptr); void Bind(int fd, const struct sockaddr* sa, socklen_t salen); void Connect(int fd, const struct sockaddr* sa, socklen_t salen); void Listen(int fd, int backlog); int Socket(int family, int type, int protocol); ssize_t Read(int fd, void* ptr, size_t nbytes); ssize_t Write(int fd, const void* ptr, size_t nbytes); void Close(int fd); ssize_t Readn(int fd, void* vptr, size_t n); ssize_t Writen(int fd, const void* vptr, size_t n); static ssize_t my_read(int fd, char* ptr); ssize_t Readline(int fd, void* vptr, size_t maxlen); #endif
#include <stdlib.h> #include <errno.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include "wrap.h" void perr_exit(const char* s) { perror(s); exit(1); } int Accept(int fd, struct sockaddr* sa, socklen_t* salenptr) { int n; again: if ( (n = accept(fd, sa, salenptr)) < 0 ) { if ((errno == ECONNABORTED) || (errno == EINTR)) { goto again; } else { perr_exit("accept error"); } } return n; } void Bind(int fd, const struct sockaddr* sa, socklen_t salen) { if ( bind(fd, sa, salen) < 0 ) { perr_exit("bind error"); } } void Connect(int fd, const struct sockaddr* sa, socklen_t salen) { if ( connect(fd, sa, salen) < 0 ) { perr_exit("connect error"); } } void Listen(int fd, int backlog) { if ( listen(fd, backlog) < 0 ) { perr_exit("listen error"); } } int Socket(int family, int type, int protocol) { int n = socket(family, type, protocol); if ( n < 0 ) { perr_exit("socket error"); } return n; } ssize_t Read(int fd, void* ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1) { if (errno == EINTR) { goto again; } else { return -1; } } return n; } ssize_t Write(int fd, const void* ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1) { if (errno == EINTR) { goto again; } else { return -1; } } return n; } void Close(int fd) { if (close(fd) == -1) { perr_exit("close error"); } } ssize_t Readn(int fd, void* vptr, size_t n) { size_t nleft; ssize_t nread; char* ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nread = read(fd, ptr, nleft)) < 0 ) { if (errno == EINTR) { nread = 0; } else { return -1; } } else if (nread == 0) { break; } nleft -= nread; ptr += nread; } } ssize_t Writen(int fd, const void* vptr, size_t n) { size_t nleft; ssize_t nwritten; const char* ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) { nwritten = 0; } else { return -1; } } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read(int fd, char* ptr) { static int read_cnt; static char* read_ptr; static char read_buf[100]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0 ) { if (errno == EINTR) { goto again; } return -1; } else if (read_cnt == 0) { return 0; } read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1; } ssize_t Readline(int fd, void* vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') { break; } } else if (rc == 0) { *ptr = 0; return n - 1; } else { return -1; } *ptr = 0; return n; } }
客户端代码(多进程多线程共用)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SRV_PORT 8000 int main(int argc, char* argv[]) { struct sockaddr_in srv_addr; int sock, conn; sock = Socket(AF_INET, SOCK_STREAM, 0); bzero(&srv_addr, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr); srv_addr.sin_port = htons(SRV_PORT); conn = Connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)); int len; char buf[MAXLINE]; while (fgets(buf, sizeof(buf), stdin)) { Write(sock, buf, strlen(buf)); len = Read(sock, buf, sizeof(buf)); if (len == 0) { printf("server is closed...\n"); break; } else { Write(STDOUT_FILENO, buf, len); } } close(sock); return 0; }
多进程服务端代码
#include <string.h> #include <netinet/in.h> #include <stdio.h> #include <unistd.h> #include <strings.h> #include <arpa/inet.h> #include <ctype.h> #include "wrap.h" #define MAXLINE 80 #define SRV_PORT 8000 int main(int argc, char* argv[]) { socklen_t cnt_len; struct sockaddr_in srv_addr, cnt_addr; int sock, conn; int n, i; pid_t pid; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; // 创建socket sock = Socket(AF_INET, SOCK_STREAM, 0); bzero(&srv_addr, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); srv_addr.sin_port = htons(SRV_PORT); // 绑定ip和端口 Bind(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)); // 开始监听 Listen(sock, 20); printf("Accepting connections ...\n"); while (1) { // 等待接收客户端连接 cnt_len = sizeof(cnt_addr); conn = Accept(sock, (struct sockaddr*)&cnt_addr, &cnt_len); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)), ntohs(cnt_addr.sin_port)); // 有新客户端连接创建子进程 pid = fork(); if (pid == 0) { // 关闭从父进程继承下来的 socket 文件描述符 Close(sock); while (1) { // 从客户端连接后产生的新文件描述符中读取数据 n = Read(conn, buf, MAXLINE); // 如果读取返回值为0证明客户端退出了 if (n == 0) { printf("client exit...\n"); break; } // 打印连接的客户端ip和端口号 printf("%s:%d", inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)), ntohs(cnt_addr.sin_port)); // 将读取到的buf数据打印到屏幕上 Write(STDOUT_FILENO, buf, n); // 将数据中所有字符转换为大写 for(i = 0; i < n; i++) { buf[i] = toupper(buf[i]); } // 重新发送给客户端 Write(conn, buf, n); } // 关闭与客户端的连接 Close(conn); return 0; } // 如果是父进程那么关闭新来的连接文件描述符并且后面什么都不做 else if (pid > 0) { Close(conn); } else { perr_exit("fork"); } } // 最后关闭socket文件描述符 Close(sock); return 0; }
编译多进程程序运行测试
编译客户端:gcc client.c wrap.c -o client
编译服务端:gcc server_fork.c wrap.c -o server_fork
运行效果:
多线程服务端代码
#include <string.h> #include <netinet/in.h> #include <stdio.h> #include <unistd.h> #include <strings.h> #include <arpa/inet.h> #include <ctype.h> #include <pthread.h> #define MAXLINE 80 #define SRV_PORT 8000 struct tag_thread { int conn; struct sockaddr_in sockaddr; }; void* recv_thread(void* arg) { // 将自身线程设置为游离态,无需再次回收 pthread_detach(pthread_self()); // 将参数转换为可识别的结构体 struct tag_thread* new_conn = (struct tag_thread*)arg; char buf[1024]; char str[INET_ADDRSTRLEN]; int len; int i; while (1) { // 从传递进来的文件描述符中读取数据 len = Read(new_conn->conn, buf, sizeof(buf)); if (0 == len) { pthread_exit(NULL); } printf("recv client data %s:%d\n", inet_ntop(AF_INET, &new_conn->sockaddr.sin_addr, str, sizeof(str)), ntohs(new_conn->sockaddr.sin_port)); // 打印读取到的数据到屏幕 Write(STDOUT_FILENO, buf, len); // 将读取到的数据转换为大写 for (i = 0; i < strlen(buf); i++) { buf[i] = toupper(buf[i]); } // 重新发回给客户端 Write(new_conn->conn, buf, len); } return (void*)0; } int main(int argc, char* argv[]) { socklen_t cnt_len; struct sockaddr_in srv_addr, cnt_addr; int sock, conn; int n, i; pid_t pid; pthread_t tid; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; sock = Socket(AF_INET, SOCK_STREAM, 0); bzero(&srv_addr, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); srv_addr.sin_port = htons(SRV_PORT); Bind(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)); Listen(sock, 20); printf("Accepting connections ...\n"); while (1) { struct tag_thread new_conn; cnt_len = sizeof(cnt_addr); // 阻塞等待新的客户端连接 conn = Accept(sock, (struct sockaddr*)&cnt_addr, &cnt_len); // 打印新来的客户端ip和端口 printf("while received from %s at PORT %d\n", inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)), ntohs(cnt_addr.sin_port)); // 设置要传递给线程函数的结构体内容 new_conn.conn = conn; // 新连接的文件描述符 new_conn.sockaddr = cnt_addr; // 新连接的属性结构体 // 创建线程 pthread_create(&tid, NULL, recv_thread, (void*)&new_conn); } Close(sock); return 0; }
编译多线程程序运行测试
编译客户端:gcc client.c wrap.c -o client
编译服务端:gcc server_thread.c wrap.c -o server_thread -l pthread
运行效果: