UDP网络聊天室程序中问题记录:
并发如何实现?
可以通过fork来实现,但并发不仅仅局限于fork。在引用中提到了利用fork来实现简单的并发服务器,这是因为fork可以创建一个与父进程一样的子进程,从而实现并发处理客户端请求。但是并发还可以通过其他方式实现,比如使用线程、进程池、协程等。
使用线程可以实现并发,线程是轻量级的执行单元,可以在同一个进程中并发执行多个任务。线程之间共享进程的资源,可以通过共享内存来进行通信。
使用进程池也可以实现并发,进程池是一组预先创建好的进程,可以重复使用来处理多个任务。进程池中的进程可以并发执行任务,从而实现并发处理。
使用协程也可以实现并发,协程是一种轻量级的线程,可以在同一个线程中实现并发执行多个任务。协程之间可以通过yield和send来进行通信。
所以,并发不仅仅是通过fork来实现,还可以使用线程、进程池、协程等方式来实现。具体选择哪种方式取决于具体的需求和场景。
-
man socket
我们使用IPV4 查看手册 man 7 ip 即可查看
-
recvfrom函数之“transport endpoint is not connected”
-
客户端client.c 不能写bind (绑定到套接字)
//bind写上会报错:bind error: Address already in use
UDP网络聊天室的实现
值得思考:server.c的do_quit()函数的写法
链表删除的时候,链表要定位在 删除结点 的前一个位置,
所以我们判断的是ptemp->next->clientaddr
这样写的话,
删除后不能执行ptemp = ptemp->next;这条语句,因为会跳过被删除结点的后一个结点。
//假设删除D
// t 当t走到d的时候,再用链表删除D,就来不及了,因为删除结点时,要找到被删除结点 的前一个结点.
// A B C D E F G
// t 所以我们要先判断t的next的位置是否是D,不是的话就先发送,再到next
// A B C D E F G
ptemp = ptemp->next; //不能写在if语句的后面,因为当把D删除之后,t会走到E的位置,而判断的时候会判断t->next也就是F 这样就会丢下E没判断
// t
// ABCEFG
代码:
server.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
// UDP聊天室 服务器
#define ERRLOG(msg) \
do \
{ \
printf("%s %s %d\n", __FILE__, __func__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
// 定义用户信息结构体
typedef struct MSG
{
char type; // 类型:L登录 C群聊 Q退出
char name[32];
char txt[128]; // 发送的内容
} msg_t;
// 定义存储客户数据的链表
typedef struct NODE
{
struct sockaddr_in clientaddr; // 数据域--客户信息
struct NODE *next; // 指针域
} node_t;
void do_login(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void do_chat(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void do_quit(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void create_node(node_t **p);
int main(int argc, char const *argv[])
{
// 入参合理性检查
if (3 != argc)
{
printf("Usage : %s <ip> <port>\n", argv[0]);
return 0;
}
// 1.创建用户数据报套接字
int sockfd = 0;
if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
ERRLOG("socket error");
// 2.填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
// 3.将套接字与服务器的网络信息结构体绑定
if (-1 == (bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)))
ERRLOG("bind error");
// 定义结构体保存客户端信息
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddr_len = sizeof(clientaddr);
msg_t msg;
// 定义链表类型
node_t *phead = NULL;
create_node(&phead);
// 4.收发数据 recvfrom sendto
pid_t pid = 0;
if (-1 == (pid = fork()))
ERRLOG("fork error");
else if (0 == pid)
{
// 子进程 循环接收数据 并处理(登录操作L 群聊操作C 退出操作Q)
// 服务器用链表来保存用户数据
while (1){
memset(&msg, 0, sizeof(msg));
if (-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len))
ERRLOG("recvfrom error"); //需要保存客户端信息
printf("客户端[%s:%d]发来数据[%s:%s]\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), msg.name, msg.txt);
switch (msg.type){
case 'L':
do_login(phead, msg, sockfd, clientaddr);
break;
case 'C':
do_chat(phead, msg, sockfd, clientaddr);
break;
case 'Q':
do_quit(phead, msg, sockfd, clientaddr);
break;
}
}
}
else if (0 < pid)
{
//负责在终端获取系统消息 并发送给所有客户端
//这时候要注意 不能直接发送 因为在子进程接收客户端消息之前
//父子进程就完成fork()了 此时父进程是什么都没有的 父子进程独立
//这里比较好的处理方式是:
// 把父进程当做一个客户端 以群聊的方式 把系统消息发给子进程(也就是服务器)
msg.type ='C';
strcpy(msg.name,"server");
while(1){
memset(msg.txt,0,128);//在终端获取字符之前要memset先清理一下
fgets(msg.txt,sizeof(msg.txt),stdin);
msg.txt[strlen(msg.txt)-1]='\0';
if(-1 == sendto(sockfd,&msg.txt,sizeof(msg.txt),0,(struct sockaddr*)&serveraddr,serveraddr_len))
ERRLOG("sendto error");
}
}
// 5.关闭套接字
close(sockfd);
return 0;
}
// 创建链表结点的函数
void create_node(node_t **p)
{
*p = (node_t *)malloc(sizeof(node_t));
memset(*p, 0, sizeof(node_t));
}
void do_login(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
//先将 xxx 加入群聊 的消息发给在线的所有人
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr)))
ERRLOG("sendto error");
}
//再将新登录的客户端加入到链表--头插
node_t *pnew = NULL;
create_node(&pnew);
pnew->clientaddr = clientaddr;
pnew->next = phead->next;
phead->next = pnew;
return ;
}
void do_chat(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
//遍历链表 将群聊的消息发送给除了自己之外的所有人
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
//需要判断不是自己的时候在sendto
if(memcmp(&clientaddr,&(ptemp->clientaddr),sizeof(clientaddr))){
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr)))
ERRLOG("sendto error");
}
}
return;
}
void do_quit(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
//将 xxx 退出群聊的消息发给其他人 将自己在链表中删除
//遍历链表,如果不是自己,就转发消息,如果是自己 就删除节点
node_t *ptemp = phead;
while(ptemp->next != NULL){
if(memcmp(&clientaddr,&(ptemp->next->clientaddr),sizeof(clientaddr))){
//不是自己 发送消息
//假设删除D
// t 当t走到d的时候,再用链表删除D,就来不及了,因为删除结点时,要找到被删除结点 的前一个结点.
// A B C D E F G
// t 所以我们要先判断t的next的位置是否是D,不是的话就先发送,再到next
// A B C D E F G
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->next->clientaddr), sizeof(ptemp->next->clientaddr)))
ERRLOG("sendto error");
ptemp = ptemp->next; //不能写在if语句的后面,因为当把D删除之后,t会走到E的位置,而判断的时候会判断t->next也就是F 这样就会丢下E没判断
// t
// ABCEFG
}else{
//是自己了 则把自己在链表里删除
node_t *pdel=ptemp->next;
ptemp->next=pdel->next;
free(pdel);
pdel=NULL;
}
}
return ;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
typedef struct MSG{
char type;//'L' 登录 'C' 群聊 'Q' 退出
char name[32];
char txt[128];
}msg_t;
int main(int argc,const char * argv[])
{
//入参合理性检查
if(3 != argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//创建套接字
int sockfd = 0;
if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
ERRLOG("socket error");
//填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//bind写上会报错:bind error: Address already in use
//客户端不能指定端口,如果客户端去指定端口会可能出现端口已经被占用导致出错,而需要修改
// × 3.将套接字与服务器的网络信息结构体绑定
// × if (-1 == (bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)))
// × ERRLOG("bind error");
msg_t msg;
memset(&msg, 0, sizeof(msg));
//输入用户名 完成登录操作
printf("请输入用户名:");
fgets(msg.name, sizeof(msg.name), stdin);
msg.name[strlen(msg.name)-1] = '\0';
msg.type = 'L';
strcpy(msg.txt, "加入群聊");
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len))
ERRLOG("sendto error");
pid_t pid = 0;
if(-1 == (pid = fork())){
ERRLOG("fork error");
}else if(0 == pid){//子进程
//负责接收服务器发来的消息并打印到终端
while(1){
memset(&msg,0,sizeof(msg));
if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL))
ERRLOG("recvfrom error");
printf("%s : %s\n", msg.name, msg.txt);
}
}else if(0 < pid){//父
//负责在终端获取聊天内容发给服务器
while(1){
//此处只需要清理txt内容即可
memset(&msg.txt,0,sizeof(msg.txt));
fgets(msg.txt,sizeof(msg.txt),stdin);
msg.txt[strlen(msg.txt)-1]='\0';
//设置一个 如果输入quit,则退出群聊
if(!strcmp(msg.txt,"quit")){
//则修改msg.type的类型为Q
msg.type = 'Q';
strcpy(msg.txt,"退出群聊");
}else{
msg.type = 'C';
}
if(-1 == sendto(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&serveraddr,serveraddr_len)){
ERRLOG("sendto error");
}
//如果接收的类型是 Q 的话
if( 'Q'== msg.type ){
//父进程退出之前,先给子进程信号让子进程退出--否则子进程就变成孤儿进程了
kill(pid, SIGKILL);
break;
}
}
}
close(sockfd);
return 0;
}