socket套接字记录
socket在日常工作中用的比较多,但是之前接触的比较少,需要总结一下,这里做一下记录:
套接字是用来数据交互的,也是一种IO操作,Unix/linux系统中,为了统一各种硬件的操作,简化接口,不同的硬件设备看成一个文件,对这些文件的操作,等同对磁盘上普通文件的操作。为了表示和区分已经打开的文件,给每个文件分配一个id,这个id是一个整数,称为文件描述符,程序在执行任何形式的io操作的时候,都是在读取或者写入一个文件描述符,一个文件描述符只是一个和打开的文件相关联的整数,他的背后可能是一个硬盘上的普通文件,终端或者是网络连接。
1、套接字的种类
套接字有很多种,例如unix套接字,internet套接字,目前用到的就这两种,主要记录这两种。
2、internet套接字
根据数据的传输方式,可以将internet套接字分成两种类型,在通过socket()
函数创建创建连接的时候,必须告诉它使用哪种数据传输方式。
- 流格式套接字(SOCK_STREAM)也叫面向连接的套接字,是一种可靠的。双向的通信数据流,数据可以准确的到达另一台计算机,损坏或者丢失,可以重新发送。(里面有自己的内部纠错机制),使用的是TCP协议,TCP协议会控制你的数据按照顺序到达并且没有错误。
- 数据报格式套接字(SOCK_DGRAM)也叫无连接的套接字,计算机只管传输数据,如果数据在传输中损坏,或者没有到达另一台计算机,是无法补救的,数据错了无法重传!
socket()
函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind()
函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect()
函数建立连接。
函数用法示例如下:
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
之后就是设置参数了,发送方和接收方要设置不同的参数:
下面是接收方设置
下面是发送方设置
可以看到这里都是用sockaddr_in
结构体,之后在强制转为sock_addr
的类型,因为sock_addr
是一种统一的标准,而sock_addr_in
是专门针对ipv4
的,相关的结构体说明如下所示:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
-
对于服务器端程序,使用
bind()
绑定套接字后,还需要使用listen()
函数让套接字进入被动监听状态,再调用accept()
函数,就可以随时响应客户端的请求了。 -
通过
listen()
函数使套接字进入被动监听状态(没有客户端连接时,套接字处于睡眠状态,只有当接收到客户端请求的时候,才会被唤醒起来响应请求) -
当套接字处于监听状态时,可以通过
accept()
函数来接收客户端请求。它的参数与listen()
和connect()
是相同的,sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
接收消息示例如下所示
accept()
返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
listen()
只是让套接字进入监听状态,并没有真正接收客户端请求,listen()
后面的代码会继续执行,直到遇到 accept()
。accept()
会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
3、主机字节序和网络字节序
前面给结构体设置参数的时候涉及到了这个部分内容,因此这里做一下补充:
主机字节序和网络字节序是两种不同的字节序。主机字节序是指在计算机内部处理数据时采用的字节序,而网络字节序是指在计算机网络中传输数据时采用的字节序。
在网络传输数据时,不同的计算机可能采用不同的主机字节序,因此需要一种公共的字节序来保证数据的正确传输。因此,网络字节序通常采用大端字节序(也称为“网络序”),即高位字节在前,低位字节在后。
比如上面用到的htons()
,htons()
用来将当前主机字节序转换为网络字节序,其中h代表主机(host)字节序,n代表网络(network)字节序,s代表short,htons 是 h、to、n、s 的组合,可以理解为”将 short 型数据从当前主机字节序转换为网络字节序。
常见的网络字节转换函数有:
- htons():host to network short,将 short 类型数据从主机字节序转换为网络字节序。
- ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
- htonl():host to network long,将 long 类型数据从主机字节序转换为网络字节序。
- ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。
是基于internet套接字的完整示例代码
server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(serv_sock, 20);
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
char str[] = "send message";
write(clnt_sock, str, sizeof(str));
close(clnt_sock);
close(serv_sock);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
char buffer[40];
read(sock, buffer, sizeof(buffer) - 1);
printf("message form server %s\n", buffer);
close(sock);
return 0;
}
可以改进一点实现循环收发:
server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(serv_sock, 20);
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
char buffer[100] = {0};
while (1)
{
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
int strlen = recv(clnt_sock, buffer, 100, 0);
send(clnt_sock, buffer, strlen, 0);
close(clnt_sock);
memset(buffer, 0 ,100);
}
close(serv_sock);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
char bufsend[100] = {0};
char bufrecv[100] = {0};
while (1)
{
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("input send message:");
scanf("%s", bufsend);
send(sock, bufsend, strlen(bufsend), 0);
recv(sock, bufrecv, 100, 0);
printf("message form server %s\n", bufrecv);
memset(bufsend, 0, 100);
memset(bufrecv, 0, 100);
close(sock);
}
return 0;
}
4、unix套接字
一般流程为: ocket函数创建了一个Unix socket,并使用bind函数将其绑定到指定的文件路径上。然后通过listen函数监听客户端连接请求,并使用accept函数接受客户端连接。最后,通过recv函数从客户端接收数据,并在控制台输出。整个过程完成后,需要使用close函数关闭套接字,释放资源。
注意,Unix socket只能用于本地进程间通信,不能用于网络通信。此外,在使用Unix socket进行进程间通信时,需要确保各进程所使用的文件路径相同,否则无法建立连接。
下面是完整示例:
client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "/tmp/socket"
int main(void)
{
int sockfd;
struct sockaddr_un servaddr;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strncpy(servaddr.sun_path, SOCKET_PATH, sizeof(servaddr.sun_path)-1);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("connect error");
exit(EXIT_FAILURE);
}
// 发送数据
char buf[] = "Hello, Unix Socket!";
ssize_t n = send(sockfd, buf, sizeof(buf), 0);
if (n == -1) {
perror("send error");
exit(EXIT_FAILURE);
}
close(sockfd);
return 0;
}
server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "/tmp/socket"
int main(void)
{
int sockfd;
struct sockaddr_un servaddr;
char buf[1024];
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strncpy(servaddr.sun_path, SOCKET_PATH, sizeof(servaddr.sun_path)-1);
unlink(SOCKET_PATH);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind error");
exit(EXIT_FAILURE);
}
if (listen(sockfd, 5) == -1) {
perror("listen error");
exit(EXIT_FAILURE);
}
for (;;) {
int connfd = accept(sockfd, NULL, NULL);
if (connfd == -1) {
perror("accept error");
continue;
}
// 处理连接请求
int nbytes = recv(connfd, buf, sizeof(buf), 0);
if (nbytes == -1)
{
perror("recv");
exit(EXIT_FAILURE);
}
printf("Received message: %s\n", buf);
close(connfd);
}
close(sockfd);
unlink(SOCKET_PATH);
return 0;
}
5、netlink套接字
netlink常用于用户态和内核态之间进行通信,下面是一个netlink套接字的示例
内核态:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>
#define NETLINK_TEST 30
#define MSG_LEN 125
#define USER_PORT 100
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhangwj");
MODULE_DESCRIPTION("netlink example");
struct sock *nlsk = NULL;
extern struct net init_net;
int send_usrmsg(char *pbuf, uint16_t len)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;
int ret;
/* 创建sk_buff 空间 */
nl_skb = nlmsg_new(len, GFP_ATOMIC);
if(!nl_skb)
{
printk("netlink alloc failure\n");
return -1;
}
/* 设置netlink消息头部 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if(nlh == NULL)
{
printk("nlmsg_put failaure \n");
nlmsg_free(nl_skb);
return -1;
}
/* 拷贝数据发送 */
memcpy(nlmsg_data(nlh), pbuf, len);
ret = netlink_unicast(nlsk, nl_skb, USER_PORT, MSG_DONTWAIT);
return ret;
}
static void netlink_rcv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = NULL;
char *umsg = NULL;
char *kmsg = "hello users!!!";
if(skb->len >= nlmsg_total_size(0))
{
nlh = nlmsg_hdr(skb);
umsg = NLMSG_DATA(nlh);
if(umsg)
{
printk("kernel recv from user: %s\n", umsg);
send_usrmsg(kmsg, strlen(kmsg));
}
}
}
struct netlink_kernel_cfg cfg = {
.input = netlink_rcv_msg, /* set recv callback */
};
int test_netlink_init(void)
{
/* create netlink socket */
nlsk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(nlsk == NULL)
{
printk("netlink_kernel_create error !\n");
return -1;
}
printk("test_netlink_init\n");
return 0;
}
void test_netlink_exit(void)
{
if (nlsk){
netlink_kernel_release(nlsk); /* release ..*/
nlsk = NULL;
}
printk("test_netlink_exit!\n");
}
module_init(test_netlink_init);
module_exit(test_netlink_exit);
用户态
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#define NETLINK_TEST 30
#define MSG_LEN 125
#define MAX_PLOAD 125
typedef struct _user_msg_info
{
struct nlmsghdr hdr;
char msg[MSG_LEN];
} user_msg_info;
int main(int argc, char **argv)
{
int skfd;
int ret;
user_msg_info u_info;
socklen_t len;
struct nlmsghdr *nlh = NULL;
struct sockaddr_nl saddr, daddr;
char *umsg = "hello netlink!!";
/* 创建NETLINK socket */
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(skfd == -1)
{
perror("create socket error\n");
return -1;
}
memset(&saddr, 0, sizeof(saddr));
saddr.nl_family = AF_NETLINK; //AF_NETLINK
saddr.nl_pid = 100; //端口号(port ID)
saddr.nl_groups = 0;
if(bind(skfd, (struct sockaddr *)&saddr, sizeof(saddr)) != 0)
{
perror("bind() error\n");
close(skfd);
return -1;
}
memset(&daddr, 0, sizeof(daddr));
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0; // to kernel
daddr.nl_groups = 0;
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
memset(nlh, 0, sizeof(struct nlmsghdr));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = 0;
nlh->nlmsg_seq = 0;
nlh->nlmsg_pid = saddr.nl_pid; //self port
memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));
ret = sendto(skfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
if(!ret)
{
perror("sendto error\n");
close(skfd);
exit(-1);
}
printf("send kernel:%s\n", umsg);
memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
ret = recvfrom(skfd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&daddr, &len);
if(!ret)
{
perror("recv form kernel error\n");
close(skfd);
exit(-1);
}
printf("from kernel:%s\n", u_info.msg);
close(skfd);
free((void *)nlh);
return 0;
}
6、套接字监听
当有很多套接字同时工作的时候,由于接收函数是阻塞的,这样是很不方便的,因此最好是能有一种类似中断的机制,于是可以使用监听的方案来提高效率,常用的有select,poll,和epoll这几种。select、poll和epoll都是Linux系统中的I/O多路复用机制,可以同时监听多个文件描述符,当其中有可读或可写事件时进行处理。
它们之间的主要区别如下:
-
select和poll的缺点:当需要监听大量的文件描述符时,每次调用select或poll都需要将所有的文件描述符从用户空间复制到内核空间,这个操作的开销比较大。此外,在处理大量文件描述符时,每次调用select或poll都需要遍历整个文件描述符集合,导致效率低下。
-
epoll的优点:epoll使用了更高效的数据结构来存储文件描述符,因此能够支持成千上万个文件描述符的监听,而不会出现性能问题。此外,epoll提供了三种工作模式(水平触发、边缘触发和信号驱动)以及更细粒度的控制选项,使其比select和poll更加灵活和强大。
-
工作方式:select和poll采用轮询方式来检查是否有事件发生,而epoll通过回调函数来通知应用程序有哪些事件已经准备好了。
-
使用场景:对于少量的连接,select和poll可能是比较适合的选择;而当需要同时处理大量的连接时,epoll是更好的选择。
下面是具体的例子:
epoll.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define MAX_CLIENTS 10
int main(int argc, char *argv[])
{
int server_sockfd, client_sockfd[MAX_CLIENTS];
struct sockaddr_in server_addr, client_addr;
int client_len = sizeof(client_addr);
int max_fd, fd_num, i, j;
struct epoll_event ev, events[MAX_CLIENTS + 1];
int epollfd;
// 创建TCP套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
// 绑定套接字到服务器地址
bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 监听客户端请求
listen(server_sockfd, MAX_CLIENTS);
// 创建epoll文件描述符
epollfd = epoll_create(MAX_CLIENTS + 1);
if (epollfd == -1)
{
perror("epoll_create");
exit(EXIT_FAILURE);
}
// 添加server_sockfd到epoll事件表
ev.events = EPOLLIN;
ev.data.fd = server_sockfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_sockfd, &ev) == -1)
{
perror("epoll_ctl: server_sockfd");
exit(EXIT_FAILURE);
}
while (1)
{
// 监听epoll事件表
fd_num = epoll_wait(epollfd, events, MAX_CLIENTS + 1, -1);
if (fd_num == -1)
{
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (i = 0; i < fd_num; i++)
{
if (events[i].data.fd == server_sockfd)
{
// 接受客户端连接请求
client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
if (client_sockfd[i] == -1)
{
perror("accept");
exit(EXIT_FAILURE);
}
// 添加client_sockfd到epoll事件表
ev.events = EPOLLIN;
ev.data.fd = client_sockfd[i];
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sockfd[i], &ev) == -1)
{
perror("epoll_ctl: client_sockfd");
exit(EXIT_FAILURE);
}
}
else
{
// 处理客户端数据
// ...
}
}
}
// 关闭套接字
close(server_sockfd);
for (j = 0; j < MAX_CLIENTS; j++)
{
if (client_sockfd[j] > 0)
{
close(client_sockfd[j]);
}
}
return 0;
}
poll.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#define MAX_CLIENTS 10
int main(int argc, char *argv[])
{
int server_sockfd, client_sockfd[MAX_CLIENTS];
struct sockaddr_in server_addr, client_addr;
int client_len = sizeof(client_addr);
int max_fd, fd_num, i, j;
struct pollfd fds[MAX_CLIENTS + 1];
// 创建TCP套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
// 绑绑定套接字到服务器地址
bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 监听客户端请求
listen(server_sockfd, MAX_CLIENTS);
// 初始化pollfd数组
fds[0].fd = server_sockfd;
fds[0].events = POLLIN;
for (i = 1; i <= MAX_CLIENTS; i++)
{
fds[i].fd = -1;
}
while (1)
{
// 监听文件描述符
fd_num = poll(fds, MAX_CLIENTS + 1, -1);
if (fds[0].revents & POLLIN)
{
// 接受客户端连接请求
client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
for (j = 1; j <= MAX_CLIENTS; j++)
{
if (fds[j].fd < 0)
{
fds[j].fd = client_sockfd[i];
fds[j].events = POLLIN;
break;
}
}
i++;
}
for (j = 1; j <= MAX_CLIENTS; j++)
{
if (fds[j].fd < 0)
{
continue;
}
if (fds[j].revents & POLLIN)
{
// 处理客户端数据
// ...
}
}
}
// 关闭套接字
close(server_sockfd);
for (j = 1; j <= MAX_CLIENTS; j++)
{
if (fds[j].fd > 0)
{
close(fds[j].fd);
}
}
return 0;
}
select.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define MAX_CLIENTS 10
int main(int argc, char *argv[])
{
int server_sockfd, client_sockfd[MAX_CLIENTS];
struct sockaddr_in server_addr, client_addr;
int client_len = sizeof(client_addr);
int max_fd, fd_num, i, j;
fd_set read_fds, all_fds;
// 创建TCP套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
// 绑定套接字到服务器地址
bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 监听客户端请求
listen(server_sockfd, MAX_CLIENTS);
// 初始化文件描述符集合
FD_ZERO(&all_fds);
FD_SET(server_sockfd, &all_fds);
max_fd = server_sockfd;
while (1) {
// 复制文件描述符集合
read_fds = all_fds;
// 监听文件描述符
fd_num = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (FD_ISSET(server_sockfd, &read_fds)) {
// 接受客户端连接请求
client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
FD_SET(client_sockfd[i], &all_fds);
if (client_sockfd[i] > max_fd) {
max_fd = client_sockfd[i];
}
i++;
}
for (j = 0; j < i; j++) {
if (FD_ISSET(client_sockfd[j], &read_fds)) {
// 处理客户端数据
// ...
}
}
}
// 关闭套接字
close(server_sockfd);
for (j = 0; j < i; j++) {
close(client_sockfd[j]);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?