关于计算机网络以及Socket编程相关的概念性知识,可以参考相关资料,这里只列举了常见函数的用法,以及两个简单的例子。
Socket 相关函数
socket 函数
创建socket(套接字)
1 2 3 #include <sys/socket.h> int socket (int family, int type, int protocol) ;
参数:
family
:协议族,通常取 AF_INET
(IPv4) 或
AF_INET6
(IPv6)
type
:套接字类型,通常取
SOCK_STREAM
(TCP) 或 SOCK_DGRAM
(UDP)
protocol
:协议,可以取IPPROTO_TCP
、IPPTOTO_UDP
,但是更建议直接取0,表示自动使用默认的协议
返回值:
创建成功:返回一个新的套接字文件描述符sockfd
创建失败:返回 -1,通过errno
获取错误信息
因此最常见的两种用法以及错误处理如下
1 2 3 4 5 6 7 8 9 10 11 12 13 int sockfd = socket(AF_INET, SOCK_STREAM, 0 );if (sockfd < 0 ) { perror("Socket creation failed" ); return -1 ; } int sockfd = socket(AF_INET6, SOCK_DGRAM, 0 );if (sockfd < 0 ) { perror("Socket creation failed" ); return -1 ; }
sockaddr 结构体
在socket网络通讯中,需要定义一个通用的sockaddr
结构体来保存对方的地址信息,sockaddr
定义如下
1 2 3 4 5 6 #include <sys/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; };
但是上述结构体太通用了,实际上我们对于IPv4使用如下更具体的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; char sin_zero[8 ]; }; struct in_addr { uint32_t s_addr; };
其中:
sin_family
:地址族,通常取
AF_INET
(IPv4)或 AF_INET6
(IPv6)
sin_port
:端口号,通常取大于1024
且小于65536
的整数,过小的端口号会被系统自动分配,或者需要sudo权限。如果将端口设置为0,表示使用随机端口,一般不推荐
sin_addr.s_addr
:IPv4
地址,例如可以取127.0.0.1
(本地地址),0.0.0.0
(任意地址,可以使用INADDR_ANY
),或者某个具体的IPv4地址
sin_zero
:填充字节,以保证和 sockaddr
大小一致,不需要设置
对于IPv6来说,需要的结构体略有不同,这里不再赘述。在传递给bind
和connect
等API时,通常会将sockaddr_in
强制转换成sockaddr
类型。
在创建socketaddr_in
结构体时,通常需要使用大小端转换和IP地址的格式转换,下面提供几个常见的例子。
服务端使用,指定任意 IP 的 PORT 端口 1 2 3 4 5 6 7 #define PORT 8080 struct sockaddr_in server_addr ;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 = INADDR_ANY;
客户端使用,指定 SERVER_IP 的 PORT 端口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define SERVER_IP "127.0.0.1" #define PORT 8080 struct sockaddr_in server_addr ;memset (&server_addr, 0 , sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0 ){ perror("Invalid address" ); return -1 ; }
bind 函数
bind 可以用于绑定套接字到指定的地址(IP
地址和端口号),用于服务器端的监听(包括TCP和UDP的服务端)。
1 2 3 #include <sys/socket.h> int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数:
sockfd
:套接字描述符
addr
:指向 sockaddr
结构体 的指针,通常由
sockaddr_in
结构体转换
addrlen
:addr
的长度,一般用
sizeof
获取
返回值:
成功:返回 0
失败:返回 -1,通过errno
获取错误信息
使用以及错误处理例如 1 2 3 4 5 if (bind(sock, (struct sockaddr *)&server_addr, sizeof (server_addr)) == -1 ) { perror("Binding failed" ); close(sock); return -1 ; }
listen 函数
对于TCP服务端,在bind之后可以使用listen函数开启监听,也就是将套接字从CLOSE状态转换为LISTEN状态。
1 2 3 #include <sys/socket.h> int listen (int sockfd, int backlog) ;
参数:
sockfd
:要操作的socket
backlog
:为服务器处于LISTEN状态下维护的已完成连接队列长度的最大值,例如5或者10(当然在系统中也存在一个上限)
返回值:
成功:返回 0
失败:返回 -1,通过errno
获取错误信息
使用以及错误处理例如 1 2 3 4 if (listen(sockfd, 5 ) == -1 ) { perror("Listen failed" ); return -1 ; }
connect 函数
connect
函数用于客户端主动连接到服务器,通常在TCP客户端的连接过程中使用(UDP也可以使用,但是主要适用于TCP)
1 2 3 #include <sys/socket.h> int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数:
sockfd
:套接字描述符
addr
:指向 sockaddr
结构体 的指针,通常由
sockaddr_in
结构体转换
addrlen
:addr
的长度,一般用
sizeof
获取
返回值:
成功:返回 0
失败:返回 -1,通过errno
获取错误信息
使用以及错误处理例如 1 2 3 4 5 if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof (server_addr)) == -1 ) { perror("Connect failed" ); close(sockfd); return -1 ; }
accept 函数
当TCP服务端调用 bind 和 listen 之后,服务器会在 accept
处阻塞,等待客户端发起连接,在连接成功后返回新的 socket
用于通信,并且可以通过输出参数获取客户端连接信息。(因为accept存在阻塞行为,TCP服务器可以使用fork多进程的方式来实现同时处理多个客户端连接,但是UDP服务器是不需要的)
1 2 3 #include <sys/socket.h> int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
参数:
sockfd
:套接字描述符
addr
:(输出参数)指向 sockaddr
结构体
的指针,通常由 sockaddr_in
结构体转换,可填
NULL,表示不获取
addrlen
:(输出参数)指向addr
长度的指针,可填
NULL,表示不获取
返回值:
成功:返回新的socket(注意这里socket也需要手动关闭)
失败:返回 -1,通过errno
获取错误信息
使用以及错误处理例如
1 2 3 4 5 6 7 8 9 10 11 12 struct sockaddr_in client_addr ;socklen_t client_len = sizeof (client_addr);int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);if (client_fd == -1 ) { perror("Accept failed" ); } printf ("Client connected: %s\n" , inet_ntoa(client_addr.sin_addr));
不获取客户端信息时,可以更加简单 1 2 3 4 5 int client_fd = accept(server_fd, NULL , NULL );if (client_fd == -1 ) { perror("Accept failed" ); }
close 函数
close函数可以用于关闭套接字,就像文件关闭一样。
1 2 3 #include <unistd.h> int close (int fd) ;
注意:
由于socket允许复制,close操作只是减少socket的引用计数,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
由于异常退出可能造成资源泄露,我们可能需要捕获ctrl+c
等触发的异常信号,以便及时释放资源。
Socket IO 函数
网络IO操作有下面几组函数:
write
/ read
writev
/ readv
send
/ recv
sendto
/ recvfrom
sendmsg
/ recvmsg
前两组是通用的IO操作,可以用于文件读写等,第二组是适合多个缓冲区的IO操作,自动针对多个缓冲区进行,多个缓冲区按照顺序连接。
这些操作也支持socket的读写操作,但是通常不适用于UDP,这里不做讨论。
我们重点关注后三组函数。
需要说明的是,
考虑到缓冲区大小限制和网络原样,发送和接收数据的时候,无法保证不会一次性发送和接收完全部数据,可能需要套一层循环,但是这里简化讨论,不考虑这个问题。
send 和 recv 函数
这两个函数是最基础的IO函数,通常只用于TCP,因为TCP是有连接的。
1 2 3 #include <sys/socket.h> ssize_t send (int sockfd, const void *buf, size_t len, int flags) ;
参数:
sockfd
:套接字描述符
buf
:发送数据缓冲区
len
:发送数据缓冲区长度
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
返回值:
成功:返回实际发送的字节数
连接关闭:返回 0
发送失败:返回 -1,通过errno
获取错误信息
使用示例 1 2 3 4 5 ssize_t sent_bytes = send(sockfd, buffer, strlen (buffer), 0 );if (sent_bytes < 0 ) { perror("send failed" ); break ; }
1 2 3 #include <sys/socket.h> ssize_t recv (int sockfd, void *buf, size_t len, int flags) ;
参数:
sockfd
:套接字描述符
buf
:(输出参数)接收数据缓冲区
len
:接收数据缓冲区长度(最大可接收的字节数)
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
返回值:
成功:返回实际接收的字节数(可能小于len
)
连接关闭:返回 0
发送失败:返回 -1,通过errno
获取错误信息
使用示例 1 2 3 4 5 6 7 8 ssize_t recv_bytes = recv(sockfd, buffer, sizeof (buffer) - 1 , 0 );if (recv_bytes < 0 ) { perror("recv failed" ); break ; } else if (recv_bytes == 0 ) { printf ("Server closed connection.\n" ); break ; }
sendto 和 recvfrom 函数
这两个函数只用于UDP,由于UDP通常是无连接的,在读写中都加入了dest_addr
和addrlen
。
1 2 3 4 #include <sys/socket.h> ssize_t sendto (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) ;
参数:
sockfd
:套接字描述符
buf
:发送数据缓冲区
len
:发送数据缓冲区长度
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
dest_addr
:接收方地址
addrlen
:接收方地址长度
返回值:
成功:返回实际发送的字节数
发送失败:返回 -1,通过errno
获取错误信息
使用例如 1 2 3 4 5 6 7 ssize_t sent_len = sendto(sockfd, message, strlen (message), 0 , (struct sockaddr *)&server_addr, sizeof (server_addr)); if (sent_len < 0 ) { perror("sendto failed" ); close(sockfd); exit (EXIT_FAILURE); }
1 2 3 4 #include <sys/socket.h> ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) ;
参数:
sockfd
:套接字描述符
buf
:(输出参数)接收数据缓冲区
len
:接收数据缓冲区长度(最大可接收的字节数)
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
src_addr
:(输出参数)发送方地址
addrlen
:(输出参数)发送方地址长度
返回值:
成功:返回实际接收的字节数(可能小于len
)
连接关闭:返回 0
发送失败:返回 -1,通过errno
获取错误信息
使用示例 1 2 3 4 5 6 7 ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0 , (struct sockaddr *)&client_addr, &addr_len); if (recv_len < 0 ) { perror("recvfrom failed" ); close(sockfd); exit (EXIT_FAILURE); }
sendmsg 和 recvmsg 函数
最后的这两个函数更具有一般性,行为更加灵活,实际上可以完全替代前两组函数,因为它引入了struct msghdr
这个结构体参数。
1 2 3 #include <sys/socket.h> ssize_t sendmsg (int sockfd, const struct msghdr *msg, int flags) ;
参数:
sockfd
:套接字描述符
msg
:消息结构体,打包了所有信息
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
返回值:
成功:返回实际发送的字节数
连接关闭:返回 0
失败:返回 -1,通过 errno
获取错误信息
1 2 3 #include <sys/socket.h> ssize_t recvmsg (int sockfd, struct msghdr *msg, int flags) ;
参数:
sockfd
:套接字描述符
msg
:(输出参数)消息结构体,打包了所有信息
flags
:标志位,用于控制读写行为,一般取0即可,也可使用
MSG_*
标志位
返回值:
成功:返回实际接收的字节数
连接关闭:返回 0
失败:返回 -1,通过 errno
获取错误信息
这里我们需要关注 iovec
和 msghdr
结构体的具体内容,iovec
结构体定义如下 1 2 3 4 5 6 #include <sys/uio.h> struct iovec { void *iov_base; size_t iov_len; };
使用例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct iovec iov [2];iov[0 ].iov_base = "Hello, " ; iov[0 ].iov_len = 7 ; iov[1 ].iov_base = "World!" ; iov[1 ].iov_len = 6 ; struct iovec iov [2];char buf1[10 ];char buf2[10 ];iov[0 ].iov_base = buf1; iov[0 ].iov_len = sizeof (buf1); iov[1 ].iov_base = buf2; iov[1 ].iov_len = sizeof (buf2);
msghdr
结构体定义如下 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/socket.h> struct msghdr { void *msg_name; socklen_t msg_namelen; struct iovec *msg_iov ; size_t msg_iovlen; void *msg_control; size_t msg_controllen; int msg_flags; };
其中的很多成员都是可选的,TCP只需要关注核心的msg_iov
以及msg_iovlen
即可,UDP还需要关注msg_name
和msg_namelen
。
TCP使用例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct iovec iov [2];char part1[] = "Hello, " ;char part2[] = "this is TCP message.\n" ;iov[0 ].iov_base = part1; iov[0 ].iov_len = strlen (part1); iov[1 ].iov_base = part2; iov[1 ].iov_len = strlen (part2); struct msghdr msg ;memset (&msg, 0 , sizeof (msg));msg.msg_iov = iov; msg.msg_iovlen = 2 ; struct iovec iov [2];char buf1[16 ];char buf2[32 ];iov[0 ].iov_base = buf1; iov[0 ].iov_len = sizeof (buf1); iov[1 ].iov_base = buf2; iov[1 ].iov_len = sizeof (buf2); struct msghdr msg ;memset (&msg, 0 , sizeof (msg));msg.msg_iov = iov; msg.msg_iovlen = 2 ;
UDP使用例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct iovec iov [2];char part1[] = "UDP message: " ;char part2[] = "Hello!" ;iov[0 ].iov_base = part1; iov[0 ].iov_len = strlen (part1); iov[1 ].iov_base = part2; iov[1 ].iov_len = strlen (part2); struct msghdr msg ;memset (&msg, 0 , sizeof (msg));msg.msg_name = &server_addr; msg.msg_namelen = sizeof (server_addr); msg.msg_iov = iov; msg.msg_iovlen = 2 ; struct iovec iov [1];char buf[64 ];iov[0 ].iov_base = buf; iov[0 ].iov_len = sizeof (buf); struct msghdr msg ;memset (&msg, 0 , sizeof (msg));msg.msg_name = &client_addr; msg.msg_namelen = sizeof (client_addr); msg.msg_iov = iov; msg.msg_iovlen = 1 ;
实例——echo服务器
我们考虑实现一个最简单的echo服务器,分别通过TCP和UDP实现。
echo服务器的逻辑很简单,将收到的信息原样发送回去,这里我们顺便在发送信息时附带了时间,在返回信息中加上了双方的地址信息。
由于TCP的accpet函数存在阻塞,使用fork多进程的方式来实现同时处理多个客户端连接。
TCP 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 #include <arpa/inet.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define PORT 8081 #define BUFFER_SIZE 1024 int server_fd;void handle_sigint (int sig) { close(server_fd); write(STDOUT_FILENO, "\nServer shutting down...\n" , 25 ); exit (0 ); } void handle_sigchld (int sig) { while (waitpid(-1 , NULL , WNOHANG) > 0 ) {} } void handle_client (int client_fd) { char buffer[BUFFER_SIZE]; ssize_t bytes_read = 0 ; struct sockaddr_in client_addr ; socklen_t addr_len1 = sizeof (client_addr); getpeername(client_fd, (struct sockaddr *)&client_addr, &addr_len1); char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof (client_ip)); int client_port = ntohs(client_addr.sin_port); struct sockaddr_in server_addr ; socklen_t addr_len2 = sizeof (server_addr); getsockname(client_fd, (struct sockaddr *)&server_addr, &addr_len2); char server_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &server_addr.sin_addr, server_ip, sizeof (server_ip)); int server_port = ntohs(server_addr.sin_port); while ((bytes_read = recv(client_fd, buffer, BUFFER_SIZE, 0 )) > 0 ) { buffer[bytes_read] = '\0' ; char response[BUFFER_SIZE + 100 ]; snprintf (response, sizeof (response), "[Server %s:%d] Received from [Client %s:%d]:\n%s" , server_ip, server_port, client_ip, client_port, buffer); write(STDOUT_FILENO, response, strlen (response)); send(client_fd, response, strlen (response), 0 ); } write(STDOUT_FILENO, "Client disconnected.\n" , 21 ); close(client_fd); exit (0 ); } int main () { signal(SIGINT, handle_sigint); signal(SIGCHLD, handle_sigchld); server_fd = socket(AF_INET, SOCK_STREAM, 0 ); if (server_fd < 0 ) { perror("Socket creation failed" ); exit (EXIT_FAILURE); } struct sockaddr_in server_addr ; memset (&server_addr, 0 , sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof (server_addr)) < 0 ) { perror("Bind failed" ); close(server_fd); exit (EXIT_FAILURE); } if (listen(server_fd, 5 ) < 0 ) { perror("Listen failed" ); close(server_fd); exit (EXIT_FAILURE); } write(STDOUT_FILENO, "TCP Echo server binding and listening on port 8080...\n" , 55 ); while (1 ) { struct sockaddr_in client_addr ; socklen_t addr_len = sizeof (client_addr); int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len); if (client_fd < 0 ) { perror("Accept failed" ); continue ; } char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof (client_ip)); int client_port = ntohs(client_addr.sin_port); char msg[100 ]; snprintf (msg, sizeof (msg), "New client connected: %s:%d\n" , client_ip, client_port); write(STDOUT_FILENO, msg, strlen (msg)); pid_t pid = fork(); if (pid == 0 ) { close(server_fd); handle_client(client_fd); } else if (pid > 0 ) { close(client_fd); } else { perror("Fork failed" ); } } close(server_fd); return 0 ; }
UDP 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <arpa/inet.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #define PORT 8081 #define BUFFER_SIZE 1024 int server_fd;void handle_sigint (int sig) { close(server_fd); write(STDOUT_FILENO, "\nServer shutting down...\n" , 25 ); exit (0 ); } int main () { signal(SIGINT, handle_sigint); server_fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (server_fd < 0 ) { perror("Socket creation failed" ); exit (EXIT_FAILURE); } struct sockaddr_in server_addr ; memset (&server_addr, 0 , sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof (server_addr)) < 0 ) { perror("Bind failed" ); close(server_fd); exit (EXIT_FAILURE); } write(STDOUT_FILENO, "UDP Echo server binding on port 8080...\n" , 41 ); char buffer[BUFFER_SIZE]; struct sockaddr_in client_addr ; socklen_t addr_len = sizeof (client_addr); while (1 ) { ssize_t bytes_received = recvfrom(server_fd, buffer, BUFFER_SIZE, 0 , (struct sockaddr *)&client_addr, &addr_len); if (bytes_received < 0 ) { perror("Receive failed" ); continue ; } buffer[bytes_received] = '\0' ; char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof (client_ip)); int client_port = ntohs(client_addr.sin_port); struct sockaddr_in server_info ; socklen_t server_len = sizeof (server_info); getsockname(server_fd, (struct sockaddr *)&server_info, &server_len); char server_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &server_info.sin_addr, server_ip, sizeof (server_ip)); int server_port = ntohs(server_info.sin_port); char response[BUFFER_SIZE + 100 ]; snprintf (response, sizeof (response), "[Server %s:%d] Received from [Client %s:%d]:\n%s" , server_ip, server_port, client_ip, client_port, buffer); write(STDOUT_FILENO, response, strlen (response)); sendto(server_fd, response, strlen (response), 0 , (struct sockaddr *)&client_addr, addr_len); } close(server_fd); return 0 ; }
客户端
使用 TCP 和 UDP
的客户端代码非常相似,因此写在了一起,使用USE_TCP
宏来区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <arpa/inet.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #define SERVER_IP "127.0.0.1" #define SERVER_PORT 8081 #define BUFFER_SIZE 1024 int sockfd;void handle_sigint (int sig) { close(sockfd); write(STDOUT_FILENO, "\nClient shutting down...\n" , 25 ); exit (0 ); } void get_timestamp (char *buffer, size_t size) { time_t now = time(NULL ); struct tm *tm_info = localtime(&now); strftime(buffer, size, "[%H:%M:%S] " , tm_info); } int main () { signal(SIGINT, handle_sigint); #ifdef USE_TCP sockfd = socket(AF_INET, SOCK_STREAM, 0 ); #else sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); #endif if (sockfd < 0 ) { perror("Socket creation failed" ); exit (EXIT_FAILURE); } struct sockaddr_in server_addr ; memset (&server_addr, 0 , sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); #ifdef USE_TCP if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof (server_addr)) < 0 ) { perror("Connection failed" ); close(sockfd); exit (EXIT_FAILURE); } #else #endif write(STDOUT_FILENO, "Connected to server. Type your messages...\n" , 44 ); char buffer[BUFFER_SIZE]; char send_buffer[BUFFER_SIZE + 20 ]; #ifdef USE_TCP #else struct sockaddr_in from_addr ; socklen_t from_len = sizeof (from_addr); #endif while (1 ) { write(STDOUT_FILENO, "You: " , 5 ); if (fgets(buffer, BUFFER_SIZE, stdin ) == NULL ) { write(STDOUT_FILENO, "\nClient exiting...\n" , 19 ); break ; } get_timestamp(send_buffer, sizeof (send_buffer)); strncat (send_buffer, buffer, BUFFER_SIZE); #ifdef USE_TCP send(sockfd, send_buffer, strlen (send_buffer), 0 ); ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0 ); #else sendto(sockfd, send_buffer, strlen (send_buffer), 0 , (struct sockaddr *)&server_addr, sizeof (server_addr)); ssize_t bytes_received = recvfrom(sockfd, buffer, BUFFER_SIZE, 0 , (struct sockaddr *)&from_addr, &from_len); #endif if (bytes_received > 0 ) { buffer[bytes_received] = '\0' ; write(STDOUT_FILENO, "Server: " , 8 ); write(STDOUT_FILENO, buffer, bytes_received); } else { write(STDOUT_FILENO, "Server disconnected.\n" , 21 ); break ; } } close(sockfd); return 0 ; }
实例——极简http服务器
这个极简的http服务器的内容很简单:
GET 请求:
主页 (/):显示欢迎信息,并提供一个跳转到 /ask 页面的链接。
问答页 (/ask):包含一个 HTML 表单,允许用户输入内容并提交。
404 处理:对于未知路径,服务器返回 404 Not Found。
来自问答页的 POST 处理:用户提交的内容将被服务器接收,并显示在返回的
HTML 页面中。
http服务器就不需要客户端了,直接使用浏览器访问即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define PORT 8081 #define BUFFER_SIZE 1024 const char *PAGE_INDEX = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Connection: close\r\n\r\n" "<html><body><h1>Welcome to the Home Page</h1>" "<a href='/ask'>Go to Ask Page</a></body></html>" ; const char *PAGE_ASK = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Connection: close\r\n\r\n" "<html><body>" "<h1>Ask Something</h1>" "<form action='/ask' method='POST'>" "<input type='text' name='question'/>" "<input type='submit' value='Ask'/>" "</form></body></html>" ; const char *PAGE_NOT_FOUND = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/html\r\n" "Connection: close\r\n\r\n" "<html><body><h1>404 Not Found</h1></body></html>" ; void handle_request (int client_fd) { char buffer[BUFFER_SIZE]; ssize_t bytes_read = read(client_fd, buffer, sizeof (buffer) - 1 ); if (bytes_read <= 0 ) { close(client_fd); return ; } buffer[bytes_read] = '\0' ; char method[10 ]; char path[100 ]; sscanf (buffer, "%s %s" , method, path); const char *response = NULL ; if (strcmp (method, "GET" ) == 0 ) { if (strcmp (path, "/" ) == 0 ) response = PAGE_INDEX; else if (strcmp (path, "/ask" ) == 0 ) response = PAGE_ASK; else response = PAGE_NOT_FOUND; } else if (strcmp (method, "POST" ) == 0 && strcmp (path, "/ask" ) == 0 ) { char *body = strstr (buffer, "\r\n\r\n" ); if (body) { body += 4 ; char question[BUFFER_SIZE] = {0 }; sscanf (body, "question=%s" , question); char response_body[BUFFER_SIZE]; snprintf (response_body, sizeof (response_body), "<html><body><h1>Your Question:</h1><p>%s</p>" "<a href='/ask'>Ask Again</a></body></html>" , question); static char response_header[BUFFER_SIZE + 100 ]; snprintf (response_header, sizeof (response_header), "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n" "Connection: close\r\n\r\n%s" , response_body); response = response_header; } } if (!response) response = PAGE_NOT_FOUND; write(client_fd, response, strlen (response)); close(client_fd); } int main () { struct sockaddr_in server_addr ; struct sockaddr_in client_addr ; socklen_t addr_len = sizeof (client_addr); int server_fd = socket(AF_INET, SOCK_STREAM, 0 ); if (server_fd < 0 ) { perror("Socket creation failed" ); exit (EXIT_FAILURE); } server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof (server_addr)) < 0 ) { perror("Bind failed" ); close(server_fd); exit (EXIT_FAILURE); } if (listen(server_fd, 5 ) < 0 ) { perror("Listen failed" ); close(server_fd); exit (EXIT_FAILURE); } printf ("HTTP Server listening on port %d...\n" , PORT); while (1 ) { int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len); if (client_fd < 0 ) { perror("Accept failed" ); continue ; } handle_request(client_fd); } close(server_fd); return 0 ; }
这里还有一个稍微复杂一点的http服务器:Tinyhttpd , 这是J.
David Blackstone在1999年写的一个不到 500 行的超轻量型 Http
Server,读一遍可以更好地理解http服务器的原理。
它除了可以根据请求返回本地目录中的文件内容,还支持
CGI:从请求中提取参数并传递给脚本执行,脚本计算并生成html返回。
补充
大小端数值转换
首先,不同的机器上对于多字节变量的字节存储顺序是不同的,有大端字节序和小端字节序两种,目前常见的机器都是小端序,但是在网络编程中统一使用大端序,也成为网络字节序。
在Linux中,提供了四个用于主机字节序和网络字节序之间相互转换的函数:
1 2 3 4 5 6 7 8 9 10 11 #include <netinet/in.h> uint16_t htons (uint16_t value) ;uint32_t htonl (uint32_t value) ;uint16_t ntohs (uint16_t value) ;uint32_t ntohl (uint32_t value) ;
IP地址格式转换
IP地址一共有两种格式:
Presentation(表达格式):也就是我们能看得懂的格式,例如"192.168.19.12"这样的字符串
Numeric(数值格式):可以存入套接字地址结构体的格式,数据类型为整型
Linux 提供了两个函数用于IP地址格式的相互转换的函数:
inet_pton()
:将IP地址从表达格式转换为数值格式
inet_ntop()
:将IP地址从数值格式转换为表达格式
1 2 3 #include <arpa/inet.h> int inet_pton (int family, const char *strptr, void *addrptr) ;
参数:
family
:地址族,通常取 AF_INET
(IPv4)或
AF_INET6
(IPv6)。
strptr
:(输入参数,表达格式)指向以点分隔的十进制(IPv4)或冒号分隔(IPv6)的
IP 地址字符串的字符指针。
addrptr
:(输出参数,数值格式)指向
struct in_addr
(IPv4)或
struct in6_addr
(IPv6)的通用指针,用于存储转换后的二进制
IP 地址。
返回值:
如果转换成功,则返回 1。
如果表达格式的 IP 地址格式有误,则返回 0。
如果转换出错则返回 -1,并设置相应的 errno
。
1 2 3 #include <arpa/inet.h> const char *inet_ntop (int family, const void *addrptr, char *strptr, size_t len) ;
参数:
family
:地址族,AF_INET
(IPv4)或
AF_INET6
(IPv6)。
addrptr
:(输入参数,数值格式)指向
struct in_addr
(IPv4)或
struct in6_addr
(IPv6)的通用指针。
strptr
:(输出参数,表达格式)用于存储转换后的 IP
地址字符串的缓冲区字符指针。
len
:strptr
的缓冲区长度。
返回值:
如果转换成功,则返回 strptr
指针。
如果出错,则返回 NULL
,并设置相应的
errno
。
输出缓冲区的大小建议设置为如下值 1 2 3 4 #include <netinet/in.h> #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
Python 简易聊天服务器
基于 Socket 编程就可以实现简单的聊天服务器,为了使用
GUI,这里选择使用 Python tkinter 实现,原理都是类似的。
pychat.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 import argparseimport loggingimport tkinter as tkfrom tkinter import simpledialog, scrolledtext, messageboximport socketimport threadingimport timelogging.basicConfig( level=logging.DEBUG, format ="%(asctime)s [%(levelname)s] %(message)s" , handlers=[ logging.FileHandler("pychat-server.log" , encoding="utf-8" ), logging.StreamHandler(), ], ) clients = {} def handle_client (client_socket, username ): """处理客户端连接""" try : clients[username] = client_socket logging.info(f"新用户加入: {username} " ) broadcast(f"{username} 加入了聊天室!" , "系统" ) while True : message = client_socket.recv(1024 ).decode("utf-8" ) if not message: break logging.debug(f"{username} 发送消息: {message} " ) broadcast(message, username) except ConnectionResetError: logging.warning(f"{username} 连接被重置" ) finally : if username in clients: del clients[username] logging.info(f"{username} 断开连接" ) broadcast(f"{username} 退出了聊天室。" , "系统" ) client_socket.close() def broadcast (message, sender ): """广播消息给所有客户端,附带时间戳""" timestamp = time.strftime("%H:%M:%S" ) formatted_message = f"[{timestamp} ] {sender} : {message} " for user, client in list (clients.items()): try : client.send(formatted_message.encode("utf-8" )) except Exception: logging.warning(f"无法向 {user} 发送消息,移除该用户" ) del clients[user] def start_server (host, port ): """启动服务器""" server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try : server.bind((host, port)) except Exception as e: logging.error(f"无法绑定到 {host} :{port} ,错误: {e} " ) return server.listen(5 ) logging.info(f"服务器启动,监听 {host} :{port} ..." ) while True : client_socket, addr = server.accept() logging.info(f"新连接: {addr} " ) username = client_socket.recv(1024 ).decode("utf-8" ).strip() if not username or username in clients: logging.warning(f"用户名 {username} 被占用,拒绝连接" ) client_socket.send("USERNAME_TAKEN" .encode("utf-8" )) client_socket.close() continue threading.Thread( target=handle_client, args=(client_socket, username), daemon=True ).start() class ChatClient : def __init__ (self, root, host, port ): self .root = root self .username = None self .client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try : self .client_socket.connect((host, port)) except Exception: messagebox.showerror("错误" , f"无法连接到服务器 {host} :{port} " ) root.quit() return self .username = self .get_username() if not self .username: root.quit() return self .client_socket.send(self .username.encode("utf-8" )) response = self .client_socket.recv(1024 ).decode("utf-8" ) if response == "USERNAME_TAKEN" : messagebox.showerror("错误" , "用户名已被占用,请重试。" ) root.quit() return self .root.title(f"聊天室 - {self.username} " ) self .chat_area = scrolledtext.ScrolledText( root, wrap=tk.WORD, state="disabled" , width=50 , height=15 ) self .chat_area.grid( row=0 , column=0 , columnspan=2 , padx=10 , pady=10 , sticky="nsew" ) self .text_input = tk.Text(root, height=4 , width=40 ) self .text_input.grid(row=1 , column=0 , padx=10 , pady=5 , sticky="nsew" ) self .button_frame = tk.Frame(root) self .button_frame.grid( row=1 , column=1 , rowspan=2 , padx=5 , pady=5 , sticky="nsew" ) self .send_button = tk.Button( self .button_frame, text="发送" , command=self .send_message ) self .send_button.pack(fill="both" , expand=True ) self .clear_button = tk.Button( self .button_frame, text="清空" , command=self .clear_input ) self .clear_button.pack(fill="both" , expand=True ) self .text_input.bind("<Return>" , lambda event: self .send_message()) self .receive_thread = threading.Thread( target=self .receive_messages, daemon=True ) self .receive_thread.start() root.grid_rowconfigure(0 , weight=3 ) root.grid_rowconfigure(1 , weight=1 ) root.grid_rowconfigure(2 , weight=0 ) root.grid_columnconfigure(0 , weight=4 ) root.grid_columnconfigure(1 , weight=1 ) def get_username (self ): """获取用户名""" return simpledialog.askstring("用户名" , "请输入您的用户名:" ) def send_message (self ): """发送消息""" message = self .text_input.get("1.0" , tk.END).strip() if message: try : self .client_socket.send(message.encode("utf-8" )) self .text_input.delete("1.0" , tk.END) except Exception: messagebox.showerror("错误" , "无法发送消息,服务器可能已断开。" ) def clear_input (self ): """清空输入框""" self .text_input.delete("1.0" , tk.END) def receive_messages (self ): """接收消息""" while True : try : message = self .client_socket.recv(1024 ).decode("utf-8" ) if not message: break self .display_message(message) except Exception: messagebox.showerror("错误" , "连接丢失。" ) self .root.quit() return def display_message (self, message ): """显示消息""" self .chat_area.config(state="normal" ) self .chat_area.insert(tk.END, message + "\n" ) self .chat_area.config(state="disabled" ) self .chat_area.yview(tk.END) def start_client (host, port ): root = tk.Tk() ChatClient(root, host, port) root.mainloop() def main (): parser = argparse.ArgumentParser(description="Python Chat Application" ) parser.add_argument( "--mode" , choices=["server" , "client" ], default="client" , help ="运行模式: server (服务端) 或 client (客户端),默认: client" , ) parser.add_argument("--host" , type =str , required=True , help ="IP 地址" ) parser.add_argument("--port" , type =int , required=True , help ="端口" ) args = parser.parse_args() if args.mode == "server" : start_server(args.host, args.port) else : start_client(args.host, args.port) if __name__ == "__main__" : main()