本博客仅对网络聊天室项目进行分享,仅供学习讨论使用,欢迎大家讨论。
UDP网络聊天室
项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件,服务器也可以自己发送通知给所有客户端。
要实现服务器广播消息,可以定义一个新的消息类型,此处我采用新的线程来实现:
// 创建服务器消息发送线程
pthread_t send_thread;
pthread_create(&send_thread, NULL, server_send_message, NULL);
线程函数
// 服务器消息发送线程函数
void* server_send_message(void* arg) {
while (1) {
struct msgserver servermsg;
servermsg.type = 's'; // 定义一个新的类型's'表示服务器消息
strcpy(servermsg.name, "Server");
// 从标准输入读取服务器要发送的消息
fgets(servermsg.text, sizeof(servermsg.text), stdin);
servermsg.text[strcspn(servermsg.text, "\n")] = '\0'; // 去除换行符
link_p temp = head->next;
while (temp != NULL) {
sendto(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr*)&temp->client_addr, sizeof(temp->client_addr));
temp = temp->next;
}
}
return NULL;
}
注意:
主线程的阻塞:在 recvfrom
函数调用时,主线程会阻塞,等待接收来自客户端的消息。这个阻塞会一直持续到有新的客户端消息到达。
发送线程:服务器的发送线程 (server_send_message
) 会持续运行,并等待用户从标准输入(键盘)输入消息,然后将消息发送给所有客户端。
这两个线程之间的执行并不会相互影响,因为它们分别在等待各自的输入:主线程等待客户端数据,发送线程函数等待服务器输入。
这种设计本质上是为了使服务器能够同时处理来自客户端的消息和服务器自己发送的消息。
服务器代码
此处使用多进程来实现,也可以使用IO多路复用,可以类比于下面的tcp网络聊天室
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
// 结构体或共用体可以放在头文件内
// 定义服务器接收客户端的信息的结构体
struct msgserver
{
int type; // 消息类型执行不同操作
char name[32]; // 用户姓名;
char text[1024]; // 消息正文
} servermsg;
char buf[1024];
// 创建新的有头单项链表结构体
typedef struct link_list
{
struct sockaddr_in client_addr; // 存储客户端信息
struct msgserver servermsg; // 存储客户端信息
struct link_list *next;
} link, *link_p;
int main(int argc, char const *argv[])
{
// 0、使用提示
if (argc != 2)
{
printf("usage:%s<port>", argv[0]);
return -1;
}
// 1、创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.\n");
return -1;
}
printf("socket sucess.\n");
// 2、绑定ip和端口号
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(server);
if (bind(sockfd, (struct sockaddr *)&server, addrlen) < 0)
{
perror("bind err.\n");
return -1;
}
printf("bind sucess.\n");
printf("wait accept client request.\n");
// 3、接受客户端数据并存储
//(1)定义发送端的结构体,不用指定,所以这里不用写其他的,接收任意端口和ip的客户端,协议不同都发不过来的!所以不用写全
//(2)创建有头单项链表
// 开辟堆区空间
link_p head = (link_p)malloc(sizeof(link));
// 容错判断
if (head == NULL) // 每次开辟堆区空间都要有容错判断
{
perror("CreateEplink error");
return -1;
;
}
// 初始化头节点,只有指针域
head->next = NULL;
// 客户端ip和端口号获取
struct sockaddr_in client;
socklen_t clientlen = sizeof(client);
//(2)判断消息类型,执行不同操作
while (1)
{
// 读取对应客户端的信息
recvfrom(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr *)&client, &clientlen);
if (servermsg.type == 'l') // 登录消息处理
{
// 添加新节点存储该客户端信息
link_p newclient = (link_p)malloc(sizeof(link));
if (newclient == NULL)
{
perror("CreateEplink error");
return -1;
}
// 固定登录语句
sprintf(servermsg.text, "--------%s login--------", servermsg.name);
// 存储对应客户端的内容
// 初始化新节点
newclient->client_addr.sin_addr = client.sin_addr; // 保存客户端地址,每一个客户端的ip都是相同的,同样的ip不需要保存,应对不同ip,所有保存
newclient->client_addr.sin_port = client.sin_port; // 保存客户端的端口号,或相同结构体可以直接赋值,相当于给每个变量分别赋值
newclient->servermsg = servermsg; // 保存客户端消息
newclient->next = head->next;
head->next = newclient;
// 服务器显示客户端登录的ip地址
printf("ip:%s port:%d join chat room\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// 向所有客户端发送登录消息
link_p temp = head->next;
while (temp != NULL)
{
if (strcmp(temp->servermsg.name, servermsg.name) != 0)
{
sendto(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr *)&temp->client_addr, clientlen);
}
temp = temp->next;
}
}
if (servermsg.type == 'q') // 退出消息处理
{
sprintf(servermsg.text, "--------%s quit--------", servermsg.name);
printf("ip:%s port:%d quit chat room\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// 广播退出消息给所有客户端
link_p temp = head->next;
while (temp != NULL)
{
if (strcmp(temp->servermsg.name, servermsg.name) != 0)
{
sendto(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr *)&temp->client_addr, clientlen);
}
temp = temp->next;
}
// 从链表中删除该客户端
link_p q = head;
link_p pdel = head->next;
while (pdel != NULL)
{
if (strcmp(pdel->servermsg.name, servermsg.name) == 0)
{
q->next = pdel->next;
free(pdel);
break;
}
else
{
q = pdel;
pdel = pdel->next;
}
}
}
if (servermsg.type == 'c') // 普通聊天消息处理
{
// 广播消息给除了自己的所有客户端
link_p temp = head->next;
while (temp != NULL)
{
if (strcmp(temp->servermsg.name, servermsg.name) != 0)
{
sendto(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr *)&temp->client_addr, clientlen);
}
temp = temp->next;
}
}
}
return 0;
}
客户端代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
// 定义发送信息的结构体
struct client
{
int type; // 消息类型执行不同操作
char name[32]; // 用户姓名;
char text[1024]; // 消息正文
} clientmsg;
// 定义接收服务器信息的结构体
struct msgserver
{
int type; // 消息类型执行不同操作
char name[32]; // 用户姓名;
char text[1024]; // 消息正文
} servermsg;
int sockfd;
int pid;
struct sockaddr_in server;
socklen_t server_len = sizeof(server);
// 按下ctrl c退出客户端
void handler(int sig)
{
clientmsg.type = 'q';
sendto(sockfd, &clientmsg, sizeof(clientmsg), 0, (struct sockaddr *)&server, server_len);
close(sockfd);
//waitpid(pid, NULL, 0);
exit(0);
}
int main(int argc, char const *argv[])
{
// 处理ctrl c退出
signal(SIGINT, handler);
if (argc != 3)
{
printf("usage:%s<post><ip>", argv[0]);
return -1;
}
// 1、创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket success.\n");
// 2、指定服务器ip地址和端口号,请求链接
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = inet_addr(argv[2]);
// 3、登录
printf("please enter your username:");
fgets(clientmsg.name, sizeof(clientmsg.name), stdin);
for (int i = 0; i < sizeof(clientmsg.text); i++)
{
if (clientmsg.name[i] == '\n')
clientmsg.name[i] = '\0';
}
clientmsg.type = 'l';
sendto(sockfd, &clientmsg, sizeof(clientmsg), 0, (struct sockaddr *)&server, server_len);
// 4、创建子进程
pid = fork();
if (pid < 0)
{
perror("fork err.");
return -1;
}
if (pid == 0) // 子进程接收
{
while (1)
{
int recv_len = recvfrom(sockfd, &servermsg, sizeof(servermsg), 0, (struct sockaddr *)&server, &server_len);
if (recv_len > 0)
{
if (servermsg.type == 'l' )
printf("%s\n", servermsg.text);
else if (servermsg.type == 'q')
{
printf("%s\n", servermsg.text);
close(sockfd);
exit(0); // 子进程退出,不打印消息
}
else
printf("%s: %s\n", servermsg.name, servermsg.text);
}
else if (recv_len == 0)
{
printf("server exit");
close(sockfd);
_exit(0);
}
else
{
perror("recv err.");
close(sockfd);
return -1;
}
}
}
else // 父进程
{
while (1)
{
fgets(clientmsg.text, sizeof(clientmsg.text), stdin);
// 去掉末尾换行符
for (int i = 0; i < sizeof(clientmsg.text); i++)
{
if (clientmsg.text[i] == '\n')
clientmsg.text[i] = '\0';
}
clientmsg.type = 'c';
if (strcmp(clientmsg.text, "quit") == 0)
{
clientmsg.type = 'q';
sendto(sockfd, &clientmsg, sizeof(clientmsg), 0, (struct sockaddr *)&server, server_len);
break;
}
else
sendto(sockfd, &clientmsg, sizeof(clientmsg), 0, (struct sockaddr *)&server, server_len);
}
}
close(sockfd);
return 0;
}
TCP网络聊天室
项目要求大抵和UDP网络聊天室类似,不过这两个是在学习函数接口时顺手创建,有很多东西没有考虑到,如有需要可以自行完善。
select
服务器代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// 客户端编号
int client_map[1024];
int client_count;
// 1、创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket success\n");
// 2、绑定ip和端口号
struct sockaddr_in ipvf;
ipvf.sin_family = AF_INET;
ipvf.sin_port = htons(atoi(argv[1]));
ipvf.sin_addr.s_addr = INADDR_ANY;
socklen_t socklen = sizeof(ipvf);
if (bind(sockfd, (struct sockaddr *)&ipvf, socklen) < 0)
{
perror("bind err.");
return -1;
}
printf("bind success\n");
// 3、启动监听
if (listen(sockfd, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen success\n");
// 4、等待接收客户端请求
#define N 1024
char buf[N] = "";
char text[N]="";
struct sockaddr_in client;
socklen_t clientlen = sizeof(client);
int acceptfd = 0;
//(1)创建表(原表、监听表),并清零
fd_set readfds, tempfds;
FD_ZERO(&readfds);
//(2)向表中添加需要监听的文件描述符
FD_SET(sockfd, &readfds); // 其实就是置一
FD_SET(0, &readfds);
//(3)定义文件描述符的最大值
int max = sockfd;
// (4)循环监听
while (1)
{
// a、将原表单赋值给监听表
tempfds = readfds;
// b、监听监听表
int ret = select(max + 1, &tempfds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select err");
return -1;
}
// c、判断表中描述符是否被操作
if (FD_ISSET(sockfd, &tempfds)) // 监听sockfd,判断是否有客户端链接
{
int acceptfd = accept(sockfd, (struct sockaddr *)&client, &clientlen);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept success\n");
printf("acceptfd:%d\n", acceptfd);
printf("ip:%s,port:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
FD_SET(acceptfd, &readfds); // 当客户端连接后,把新创建的acceptfd文件描述符保存到监听表中
if (max < acceptfd) // 更改文件描述符最大值,让其监听到,因为从0开始下标
max = acceptfd;
// 保存客户端的姓名
client_map[acceptfd] = ++client_count;
}
else if (FD_ISSET(0, &tempfds))
{
fgets(buf, sizeof(buf), stdin);
for (int i = 4; i <= max; i++) // 服务器遍历发送给所有客户端
{
if (FD_ISSET(i, &readfds)) // 判断文件描述符是否在原表内为1
{
send(i, buf, sizeof(buf), 0);
}
}
memset(buf, 0, sizeof(buf));
}
for (int i = 4; i <= max; i++) // 遍历所有客户端是否有消息发送过来
{
if (FD_ISSET(i, &readfds))//客户端是否存在原表内,即是否在线
{
if (FD_ISSET(i, &tempfds))//某一个客户端有消息
{
int ret = recv(i, buf, sizeof(buf), 0);
if (ret > 0)
{
printf("client %d:%s", client_map[i], buf);
sprintf(text, "client %d:%s", client_map[i], buf);
int len=sizeof(text);
for (int j = 4; j <= max; j++) // 除了自己和不在的客户端都发送信息
{
if (j!=i&&FD_ISSET(j,&readfds))
{
send(j, text, len, 0);
}
}
memset(buf, 0, sizeof(buf));
}
else if (ret == 0)
{
printf("client%d exit\n", client_map[i]);
close(i);
FD_CLR(i, &readfds); // 从原表中删除退出的客户端
while (!FD_ISSET(max, &readfds)) // 减少文件描述符至最大的,保证遍历最大个数最少
{
max--;
}
}
else
{
perror("recv err");
break;
}
}
}
}
}
close(sockfd);
for(int i=4;i<=max;i++)//关闭所有文件描述符
close(i);
return 0;
}
客户端代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// 1、创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket success\n");
// 2、申请连接
struct sockaddr_in server;
server.sin_family=AF_INET;//
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = inet_addr(argv[2]);
socklen_t slen = sizeof(server);
if (connect(sockfd, (struct sockaddr *)&server, slen) < 0)
{
perror("connect err");
return -1;
}
printf("connect success\n");
#define N 1024
char buf[N] = "";
char text[N] = "";
//(1)创建表(原表、监听表),并清零
fd_set readfds, tempfds;
FD_ZERO(&readfds);
//(2)向表中添加需要监听的文件描述符
FD_SET(sockfd, &readfds); // 其实就是置一
FD_SET(0, &readfds);
//(3)定义文件描述符的最大值
int max = sockfd;
// (4)循环监听
while (1)
{
// a、将原表单赋值给监听表
tempfds = readfds;
// b、监听监听表
int ret = select(max + 1, &tempfds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select err");
return -1;
}
// c、判断表中描述符是否被操作
if (FD_ISSET(sockfd, &tempfds)) // 监听sockfd,判断是服务器是否发送消息
{
int ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
break;
}
else if (ret > 0)
{
printf("%s",buf);
memset(buf, 0, sizeof(buf));
}
else
{
printf("server exit\n");
return -1;
}
}
else if (FD_ISSET(0, &tempfds))//监听客户端是否发送消息
{
fgets(buf, sizeof(buf), stdin);
send(sockfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
}
close(sockfd);
return 0;
}
poll
服务器代码
#include <stdio.h>
#include <poll.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("usage:%s<port>", argv[0]);
return -1;
}
// 1、创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket success\n");
// 2、绑定ip和端口号
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = INADDR_ANY;
socklen_t socklen = sizeof(server);
if (bind(sockfd, (struct sockaddr *)&server, socklen) < 0)
{
perror("bind err");
return -1;
}
printf("bind sucess\n");
// 3、启动监听
if (listen(sockfd, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen success\n");
// 3、等待连接
struct sockaddr_in client;
socklen_t clientlen = sizeof(client);
int acceptfd = 0;
// 4、创建poll表
struct pollfd fds[100];
// 5、填表
int last = -1; //定义遍历的最大值
fds[++last].fd = sockfd;
fds[last].events = POLLIN;
fds[++last].fd = 0;
fds[last].events = POLLIN;
// 6、循环监听
#define N 1024
char buf[N];
char text[N];
int num[N];
int num_count = 0;
while (1)
{
poll(fds, last + 1, -1);
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == sockfd)
{
acceptfd = accept(sockfd, (struct sockaddr *)&client, &clientlen);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("accept success\n");
printf("acceptfd:%d\n", acceptfd);
fds[++last].fd = acceptfd;
fds[last].events = POLLIN;
fds[last].revents = 0; // 将 revents 字段初始化为 0。
//revents 是 poll 返回时填充的字段,表示实际发生的事件。
//初始化为 0 是为了确保在 poll 调用之前,revents 不包含任何之前的事件,避免误判或干扰。
// 打印客户端ip和端口
num[last] = ++num_count;
printf("ip:%s port:%d join chat room\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
else if (fds[i].fd == 0)
{
fgets(buf, sizeof(buf), stdin); // 这个地方要写stdin,要不然可能无法正确访问
for (int i = 0; i < sizeof(buf); i++)
{
if (buf[i] == '\n')
buf[i] = '\0';
}
sprintf(text, "server:%s", buf);
for (int j = 2; j <= last; j++)
{
// if (fds[j].revents == POLLIN) //这一步不需要,因为是保证了所有的客户端都在线,
//{
send(fds[j].fd, text, sizeof(text), 0);
//}
}
memset(buf, 0, sizeof(buf));
memset(text, 0, sizeof(text));
}
else // 接受并发送给所有客户端,除了不管还能有啥方法呢---遍历表中的fd去判断是否相等???怎么实现呢?
{
int ret = recv(fds[i].fd, buf, sizeof(buf), 0);
if (ret > 0)
{
printf("client:%d:%s\n", num[i], buf);
sprintf(text, "client %d:%s", num[i], buf);
for (int j = 2; j <= last; j++) // 从数组下标2开始遍历,重新遍历一次更换的文件描述符
{
if (j != i)
{
send(fds[j].fd, text, sizeof(text), 0);
}
}
memset(buf, 0, sizeof(buf));
memset(text, 0, sizeof(text));
}
else if (ret == 0)
{
printf("client%d exit\n", num[i]);
close(fds[i].fd);
num[i] = num[last];// 将最后一个num数组的元素复制到当前元素,更新标号,否则会一直遍历num[3]=1,因为最后一个删除了到不了4
fds[i--] = fds[last--];//将最后一个结构体赋值给删除的结构体,同时循环次数-1,i-1,重新问一次当前文件描述符有没有动作,保证正确性
}
else
{
perror("recv err");
return -1;
}
}
}
}
}
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
close(fds[i].fd);
}
}
return 0;
}
客户端代码
#include <stdio.h>
#include <poll.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("usage:%s<port><ip>", argv[0]);
return -1;
}
// 1、创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket success\n");
// 2、申请连接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = inet_addr(argv[2]);
socklen_t socklen = sizeof(server);
if (connect(sockfd, (struct sockaddr *)&server, socklen))
{
perror("connect err");
return -1;
}
printf("connect success\n");
// 4、创建poll表
struct pollfd fds[100];
// 5、填表
int last = -1;
fds[++last].fd = sockfd;
fds[last].events = POLLIN;
fds[++last].fd = 0;
fds[last].events = POLLIN;
// 6、循环监听
#define N 1024
char buf[N];
while (1)
{
poll(fds, last + 1, -1);
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == sockfd)
{
int ret = recv(fds[i].fd, buf, sizeof(buf), 0);
if (ret > 0)
{
printf("%s\n", buf);
memset(buf, 0, sizeof(buf));
}
else if (ret == 0)
{
close(fds[i].fd);
fds[i--] = fds[last--];
}
else
{
perror("recv err");
return -1;
}
}
else if (fds[i].fd == 0)
{
fgets(buf, sizeof(buf), stdin); // 这个地方要写stdin,要不然可能无法正确访问
for (int i = 0; i < sizeof(buf); i++)
{
if (buf[i] == '\n')
buf[i] = '\0';
}
send(sockfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
}
}
}
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
close(fds[i].fd);
}
}
return 0;
}