创建一个TCP服务器的基本操作:
函数原型:
int socket(int domain, int type, int protocol);
参数说明:
返回值: 如果成功,返回一个非负整数,表示套接字文件描述符(socket file descriptor)。如果失败,返回-1,并设置全局变量errno来指示错误类型。
定义如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,一般为 AF_INET
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 用于补齐,一般设置为全0
};
其中,sa_family_t 和 in_port_t 是整数类型,struct in_addr 是一个用于存储 IPv4 地址的结构体。sin_family 表示地址族,一般为 AF_INET,表示使用 IPv4 地址。sin_port 表示端口号,用于标识网络中的应用程序。sin_addr 存储了 IPv4 地址的信息。sin_zero 是一个用于补齐的字段,一般设置为全0。
它的定义如下:
struct in_addr {
in_addr_t s_addr; // IPv4 地址
};
其中,in_addr_t 是一个无符号整数类型,用于存储 IPv4 地址。
函数原型如下:
uint16_t htons(uint16_t hostshort);
在 C 语言中,bind 函数的原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
返回值:
在 C 语言中,listen 函数的原型如下:
int listen(int sockfd, int backlog);
参数说明:
返回值:
在 C 语言中,accept 函数的原型如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
返回值:
在 C 语言中,recv 函数的原型如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数含义:
返回值: recv 函数会阻塞程序执行,直到有数据到达为止。当有数据到达时,recv 函数会将数据从套接字读取到指定的缓冲区 buf 中,并返回实际接收到的字节数。如果返回值为 0,表示对端已关闭连接。如果返回值为 -1,表示接收数据的操作未成功。
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
int main()
{
//创建socket,参数说明:
//指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
//指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//绑定
//sockaddr_in 是一个用于表示 IPv4 地址的结构体
struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
server_info.sin_family = AF_INET; //地址族
server_info.sin_port = htons(7000); //端口,大于1024都行
// server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(3);
}
printf("等待客户端的连接...\n");
//接受连接
struct sockaddr_in client_info;
int length = sizeof(client_info);
int fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
if(-1 == fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n", fd);
char buf[1024] = {0};
ssize_t size;
while(1)
{
size = recv(fd, buf, sizeof(buf), 0);
if(size == -1)
{
perror("recv");
break;
}
else if(size == 0)
break;
if(!strcmp(buf, "bye"))
break;
printf("%s\n", buf);
bzero(buf, 1024);
}
close(fd); //关闭TCP连接,不能在接受数据
close(sockfd); //关闭socket,不能再处理客户端的请求
//socket用于处理客户端的连接,fd用于处理客户端的消息
return 0;
}
客户端连接服务端的基本操作:
函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
返回值: 函数返回一个 ssize_t 类型的值,表示实际发送的字节数。如果发送失败,返回值为 -1,并且可以通过检查全局变量 errno 来获取错误代码。
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//发起连接
struct sockaddr_in server_info; // 存储服务器信息
bzero(&server_info, 0);
server_info.sin_family = AF_INET; //地址族
server_info.sin_port = htons(7000); //绑定端口
server_info.sin_addr.s_addr = inet_addr("192.168.175.129"); //绑定ip地址
if(connect(sockfd, (struct sockaddr*)&server_info, sizeof(server_info)) == -1)
{
perror("connect");
exit(2);
}
//发送信息
char buf[1024] = {0};
while(1)
{
scanf("%s", buf);
if(send(sockfd, buf, sizeof(buf), 0) == -1)
{
perror("send");
exit(3);
}
if(!strcmp(buf, "bye"))
break;
bzero(buf, sizeof(buf));
}
close(sockfd);
return 0;
}
使用多线程来实现响应多个客户端
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
void *client_recv(void *arg)
{
int fd = *(int *)arg;
char buf[1024] = {0};
ssize_t size;
while(1)
{
size = recv(fd, buf, sizeof(buf), 0);
if(size == -1)
{
perror("recv");
break;
}
else if(size == 0)
break;
if(!strcmp(buf, "bye"))
break;
printf("%s\n", buf);
bzero(buf, 1024);
}
printf("客户端 %d 退出\n", fd);
close(fd);
}
int main()
{
//创建socket,参数说明:
//指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
//指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//绑定
//sockaddr_in 是一个用于表示 IPv4 地址的结构体
struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
server_info.sin_family = AF_INET; //地址族
server_info.sin_port = htons(7000); //端口,大于1024都行
// server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(3);
}
printf("等待客户端的连接...\n");
//接受连接
struct sockaddr_in client_info;
int length = sizeof(client_info);
int fd;
while(1)
{
fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
if(-1 == fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n", fd);
//为每个客户端创建一个线程
pthread_t tid;
if(pthread_create(&tid, NULL, client_recv, &fd) != 0)
{
perror("pthread_create");
exit(4);
}
pthread_detach(tid);
}
// close(fd); //关闭TCP连接,不能在接受数据
close(sockfd); //关闭socket,不能再处理客户端的请求
//socket用于处理客户端的连接,fd用于处理客户端的消息
return 0;
}
服务器代码:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
typedef struct Info
{
char text[1024];
int tofd;
}Info;
void *client_recv(void *arg)
{
int fd = *(int *)arg;
Info buf;
ssize_t size;
while(1)
{
size = recv(fd, &buf, sizeof(buf), 0);
if(size == -1)
{
perror("recv");
break;
}
else if(size == 0)
break;
if(!strcmp(buf.text, "bye"))
break;
//转发数据
if(send(buf.tofd, &buf, size, 0) == -1)
{
perror("send");
break;
}
bzero(&buf, sizeof(buf));
}
printf("客户端 %d 退出\n", fd);
close(fd);
}
int main()
{
//创建socket,参数说明:
//指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
//指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//绑定
//sockaddr_in 是一个用于表示 IPv4 地址的结构体
struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
server_info.sin_family = AF_INET; //地址族
server_info.sin_port = htons(7000); //端口,大于1024都行
// server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(3);
}
printf("等待客户端的连接...\n");
//接受连接
struct sockaddr_in client_info;
int length = sizeof(client_info);
int fd;
while(1)
{
fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
if(-1 == fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n", fd);
//为每个客户端创建一个线程
pthread_t tid;
if(pthread_create(&tid, NULL, client_recv, &fd) != 0)
{
perror("pthread_create");
exit(4);
}
pthread_detach(tid);
}
// close(fd); //关闭TCP连接,不能在接受数据
close(sockfd); //关闭socket,不能再处理客户端的请求
//socket用于处理客户端的连接,fd用于处理客户端的消息
return 0;
客户端代码:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
typedef struct Info
{
char text[1024];
int tofd;
}Info;
pthread_t tid[2] = {0};
void *send_thread(void *arg)
{
int sockfd = *(int *)arg;
Info buf;
while(1)
{
scanf("%s%d", buf.text, &buf.tofd);
if(send(sockfd, &buf, sizeof(buf), 0) == -1)
{
perror("send");
break;
}
if(!strcmp(buf.text, "bye"))
break;
bzero(&buf, sizeof(buf));
}
}
void *recv_thread(void *arg)
{
int sockfd = *(int *)arg;
Info buf;
ssize_t size;
while(1)
{
size = recv(sockfd, &buf, sizeof(buf), 0);
if(size == -1)
{
perror("recv");
break;
}
else if(size == 0)
break;
printf("%s\n", buf.text);
bzero(&buf, sizeof(buf));
}
}
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//发起连接
struct sockaddr_in server_info; // 存储服务器信息
bzero(&server_info, 0);
server_info.sin_family = AF_INET; //地址族
server_info.sin_port = htons(7000); //绑定端口
server_info.sin_addr.s_addr = inet_addr("192.168.175.129"); //绑定ip地址
if(connect(sockfd, (struct sockaddr*)&server_info, sizeof(server_info)) == -1)
{
perror("connect");
exit(2);
}
//启动2个线程,一个负责发送,一个负责接收
if(pthread_create(&tid[0], NULL, send_thread, &sockfd) != 0)
{
perror("pthread_create");
exit(3);
}
if(pthread_create(&tid[1], NULL, recv_thread, &sockfd) != 0)
{
perror("pthread_create")
exit(4);
}
void *status;
pthread_join(tid[0], &status);
pthread_join(tid[1], &status);
close(sockfd);
return 0;
}
创建UDP服务器的步骤:
代码:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
//绑定
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(6000);
server_info.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
ssize_t size;
char buf[1024] = {0};
struct sockaddr_in client_info;
int len = sizeof(client_info);
while(1)
{
size = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_info, &len);
if(-1 == len)
{
perror("recvfrom");
break;
}
printf("收到 %s : %d 的数据 %s\n", inet_ntoa(client_info.sin_addr), client_info.sin_port, buf);
bzero(buf, 1024);
}
close(sockfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
char buf[1024] = {0};
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(6000);
server_info.sin_addr.s_addr = inet_addr("127.0.0.1");
while(1)
{
scanf("%s", buf);
ssize_t size = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_info, sizeof(server_info));
if(-1 == size)
{
perror("send");
break;
}
bzero(buf, 1024);
}
close(sockfd);
return 0;
}
使用 select 的一般步骤如下:
select函数的原型:
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
返回值:
概念: fd_set 是一个集合数据结构。它用于在 C 语言中表示一组文件描述符(file descriptor)的集合。
fd_set 是一个位向量(bit vector),用于表示文件描述符的集合。它通常是一个固定大小的数组,每个元素都是一个位字段(bit field),用于表示一个文件描述符的状态。每个位对应一个文件描述符,如果该位为 1,则表示对应的文件描述符在集合中,如果该位为 0,则表示对应的文件描述符不在集合中。
fd_set 的一些常用操作和相关的函数:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(7000);
server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(3);
}
printf("等待客户端的连接...\n");
//定义两个集合
fd_set readset, tmpset;
//初始化
FD_ZERO(&readset);
FD_ZERO(&tmpset);
//添加文件描述符
FD_SET(sockfd, &readset);
int maxfd = sockfd;
int fd[1024] = {0};
int ret, i;
while(1)
{
tmpset = readset;
ret = select(maxfd + 1, &tmpset, NULL, NULL, NULL);
if(-1 == ret)
{
perror("select");
break;
}
if(FD_ISSET(sockfd, &tmpset)) //判断sockfd是否留在集合中(是否有客户端发起连接)
{
struct sockaddr_in client_info; //用于保存客户端信息
int length = sizeof(client_info);
//找到一个没有分配的fd
for(i = 0; i < 1024; i++)
{
if(0 == fd[i])
break;
}
fd[i] = accept(sockfd, (struct sockaddr *)&client_info, &length);
if(-1 == fd[i])
{
perror("accept");
break;
}
printf("接受客户端的连接:%d\n", fd[i]);
//把新的文件描述符加入集合中
FD_SET(fd[i], &readset);
//更新最大文件描述符
if(maxfd < fd[i])
maxfd = fd[i];
}
else
{
for(i = 0; i < 1024; i++)
{
if(FD_ISSET(fd[i], &tmpset))
{
char buf[1024] = {0};
ssize_t size;
size = recv(fd[i], buf, sizeof(buf), 0);
if(size == -1)
{
perror("recv");
}
else if(size == 0)
{
printf("客户端 %d 退出\n", fd[i]);
FD_CLR(fd[i], &readset); // 从集合中删掉
close(fd[i]);
fd[i] = 0;
}
else
printf("%s\n", buf);
break;
}
}
}
}
close(sockfd);
return 0;
}
概念: epoll 是一个在 Linux 操作系统上用于高效事件通知的 I/O 多路复用机制。它是一种替代传统的 select 和 poll 函数的方法,可以在大规模并发连接的网络服务器中提供更高的性能。
使用 epoll 的一般步骤如下:
int epoll_create(int size);
这个函数会返回一个文件描述符,用于后续的 epoll 操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd 是 epoll 实例的文件描述符,op 是操作类型(如添加、修改或删除),fd 是要添加的文件描述符,event 是一个 epoll_event 结构体,用于指定事件类型和关联的数据
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd 是 epoll 实例的文件描述符,events 是一个数组,用于存储发生的事件,maxevents 是数组的大小,timeout 是等待的超时时间(-1 表示无限等待)。
处理发生的事件:
遍历 epoll_wait 返回的事件数组,根据事件类型进行相应的处理。
关闭 epoll 实例
int close(int fd);
概念: epoll_create 函数用于创建一个 epoll 实例,并返回一个文件描述符,以便后续的 epoll 操作。
它的函数原型如下:
int epoll_create(int size);
参数说明:
返回值:
epoll_event 是一个结构体类型,在 Linux 中使用 epoll 进行 I/O 多路复用时,用于表示事件的数据结构。
它的定义如下:
struct epoll_event {
uint32_t events; // 表示事件类型的位掩码
epoll_data_t data; // 与事件相关的用户数据
};
events:一个无符号 32 位整数,用于表示事件类型的位掩码。可以使用以下常量进行设置:
data:一个 epoll_data_t 类型的联合体,用于存储与事件相关的用户数据。epoll_data_t 的定义如下:
typedef union epoll_data {
void *ptr; // 指针类型的用户数据
int fd; // 文件描述符类型的用户数据
uint32_t u32; // 32 位无符号整数类型的用户数据
uint64_t u64; // 64 位无符号整数类型的用户数据
} epoll_data_t;
epoll_ctl 是 Linux 中使用 epoll 进行事件注册和控制的系统调用。它用于向 epoll 实例中添加、修改或删除事件。
epoll_ctl 的原型如下:
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
返回值:
epoll_wait 是 Linux 中使用 epoll 进行事件等待和处理的系统调用。它用于等待 epoll 实例中注册的事件发生,并返回就绪的事件信息。
epoll_wait 的原型如下:
#include
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
返回值:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(1);
}
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(7000);
server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("bind");
exit(2);
}
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(3);
}
printf("等待客户端连接...\n");
//创建epoll对象(创建集合)
int epfd = epoll_create(1);
if(-1 == epfd)
{
perror("epoll_create");
exit(4);
}
//把sockfd封装成事件,添加到集合中
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; //事件属性,边缘触发
ev.data.fd = sockfd;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1)
{
perror("epoll_ctl");
exit(5);
}
while(1)
{
struct epoll_event events[10];
int num = epoll_wait(epfd, events, 10, -1);
if(-1 == num)
{
perror("epoll_wait");
break;
}
for(int i = 0; i < num; i++)
{
if(events[i].data.fd == sockfd) //有客户端发起连接
{
struct sockaddr_in server_info;
int length = sizeof(server_info);
int fd = accept(sockfd, (struct sockaddr *)&server_info, &length);
if(-1 == fd)
{
perror("accept");
break;
}
printf("客户端 %d 连接成功\n", fd);
ev.events = EPOLLIN; //默认水平触发
ev.data.fd = fd;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
{
perror("epoll_ctl");
break;
}
}
else
{
char buf[1024] = {0};
ssize_t size;
size = recv(events[i].data.fd, buf, sizeof(buf), 0);
if(size == -1)
perror("recv");
else if(size == 0)
{
if(epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]) == -1)
{
perror("epoll_ctl");
exit(5);
}
close(events[i].data.fd);
printf("客户端 %d 退出\n", events[i].data.fd);
}
else
printf("%s\n", buf);
}
}
}
close(sockfd);
return 0;
}