// 并发服务器-线程
#include <stdio.h>
#include <strings.h> //bzero
#include <unistd.h> //close
#include <sys/socket.h> //socket
#include <netinet/in.h> //struct sockaddr_in
#include <arpa/inet.h> //inet_addr
#include <string.h>
#include <pthread.h>
void *recv_send_fun(void *arg)
{
int sockfd_new = *((int *)arg);
char buf[1024] = "";
while (1)
{
bzero(buf, sizeof(buf));
recv(sockfd_new, buf, sizeof(buf), 0);
printf("recv: %s\n", buf);
send(sockfd_new, "ok", sizeof("ok"), 0);
if (strcmp(buf, "quit") == 0)
break;
// 如果收到了0长度的数据包,意思客户端断开连接,子进程结束
if (strlen(buf) == 0)
break;
}
return NULL;
}
int main(int argc, char const *argv[])
{
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
// 端口复用
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes));
// 2.绑定bind
struct sockaddr_in my_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 机器的所有可用IP地址
int ret = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (ret != 0)
{
perror("bind");
return -1;
}
// 3.监听
int backlog = 10;
ret = listen(sockfd, backlog);
if (ret < 0)
{
perror("listen");
return -1;
}
// 4.提取,等待一个客户端连接
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
char ip[16] = "";
int sockfd_new = 0;
while (1)
{
// 保证提取到了正确的客户端,而不是被信号打断
while (1)
{
sockfd_new = accept(sockfd, (struct sockaddr *)&cli_addr, &len);
if (sockfd_new > 0)
break;
}
inet_ntop(AF_INET, (void *)&cli_addr.sin_addr, ip, 16);
printf("提取到的客户端: %s--->%hu\n", ip, ntohs(cli_addr.sin_port)); // ip port
// 5.创建线程
pthread_t pth;
pthread_create(&pth, NULL, recv_send_fun, (void *)&sockfd_new);
pthread_detach(pth);
}
// 6.关闭套接字
close(sockfd_new);
close(sockfd);
return 0;
}
- 创建服务器套接字(socket)。
- 设置端口复用选项(SO_REUSEADDR)。
- 绑定(bind)套接字到服务器的端口和地址。
- 监听(listen)到来的连接请求。
- 接受(accept)连接请求创建新的套接字为客户端服务。
- 为每个客户端连接创建一个新线程。
- 在新线程中接收(recv)客户端数据并发送(send)响应数据。
- 一旦客户端发送 “quit” 或者断开连接,关闭该客户端的套接字。
- 主线程继续接受新的客户端连接。
- 当服务器停止时,它会关闭主套接字。
bzero 函数已在新版的 POSIX 标准中被弃用,推荐使用 memset 函数来替代它初始化缓冲区。
- 错误处理:在创建套接字、绑定、监听等步骤中,如果发生错误,应关闭套接字并退出程序。
recv 和 send 函数的返回值没有被检查。在实践中,你应该检查这些函数的返回值以确定操作是否成功,以及是否在遇到特定错误时关闭连接。
- 在多线程环境中直接使用
printf 可能会导致输出混乱,因为多个线程可能会同时尝试写入到标准输出。在多线程应用中通常需要同步输出。
sockfd_new 在主线程和子线程中共享,这可能会导致竞态条件。正确的做法是为每个子线程单独分配一个 sockfd_new 的副本,可以通过动态分配或传递线程函数的副本来完成。
- 传递指向
sockfd_new 的指针给线程可能是危险的,因为它是在栈上分配的,主线程可能会修改它之后的值,解决方法是在创建线程时传递其值的拷贝,而不是指针。
- 当客户端关闭连接时,
recv 函数会返回 0,所以检查 strlen(buf) 是否为 0 是重复的。
- 在正式的实现中,应当包含信号处理,例如对于
SIGCHLD 来回收子线程资源,虽然使用 pthread_detach 已经允许线程结束时自动释放资源。
- 当
recv_send_fun 函数中的循环结束时,它应当关闭它的连接套接字 sockfd_new 而不是在 main 函数的最后。
- 当服务器停止接受新的连接时,应该有一个机制来关闭所有的剩余客户端连接并清理资源,这在你的代码中没有提现出来。
#include <strings.h> 应该是 #include <string.h>,包含了 bzero 函数的正确头文件是 string.h。