需求:
创建基于UDP的网络聊天室服务器与客户端
服务器的功能:
随时转发客户端的登录、群聊及退出请求
随时发送服务器信息
客户端的功能:
随时发送登录 群聊 下线的请求
随时接受服务器发送的信息
代码实现过程:
SerUdpchat:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define IP "192.168.1.164"
#define PORT 6666
#define MSG_ERR(msg){\
fprintf(stderr,"line : %d\n",__LINE__);\
perror("msg");\
}
//重命名节点结构体数据类型
//定义结点结构体类型
typedef struct Node{
union{
int len; //头结点数据域,可以不用,也可以用于存放结点长度
struct sockaddr_in data; //普通结点数据域,存放数据项
};
struct Node *next; //指针域,指向下一个结点的结点指针
}LinkList;
socklen_t addrlen = 0;
char buf[128] = "";
ssize_t res = 0;
int sfd = 0;
LinkList *L = NULL;
LinkList *q1 = NULL;
LinkList *q2 = NULL;
LinkList *tempq = NULL;
struct sockaddr_in sein;
//数据协议
struct rule{
char type;
char name[32];
char text[128];
}msg;
//创建链表
LinkList *list_create();
//链表判空
int list_ifempt(LinkList *L);
//申请结点封装数据函数
LinkList *list_NodeIn(struct sockaddr_in e);
//链表 头插入
int list_insert_head(LinkList *L,struct sockaddr_in e);
//recvfrom处理函数
void* funcR(void* arg);
//sendto处理函数
void* funcS(void* arg);
int main(int argc, const char *argv[])
{
L = list_create();
if(NULL == L){
MSG_ERR(list_create);
return -1;
}
//创建流式套接字
sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd < 0){
MSG_ERR(socket);
return -1;
}
//允许端口快速重用成功
int reuse = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) <0)
{
MSG_ERR(setsockopt);
return -1;
}
printf("允许端口快速重用成功\n");
//填充服务器地址信息结构体
sein.sin_family = AF_INET;
sein.sin_port = htons(PORT);
sein.sin_addr.s_addr = inet_addr(IP);
addrlen = sizeof(sein);
//绑定服务器IP和端口
if(bind(sfd,(struct sockaddr*)&sein,sizeof(sein)) < 0){
MSG_ERR(bind);
return -1;
}
char buf[128] = "";
ssize_t res = 0;
struct sockaddr_in cin;//存储接收到的数据包的地址信息
//接收信息的线程
pthread_t tidR;
if(pthread_create(&tidR,NULL,funcR,&cin) < 0){
MSG_ERR(pthread_create);
return -1;
}
//发送系统信息的线程
pthread_t tidS;
if(pthread_create(&tidS,NULL,funcS,NULL) < 0){
MSG_ERR(pthread_create);
return -1;
}
while(1);
return 0;
}
//recvfrom处理函数
void* funcR(void* arg){
//获得空的客户端地址信息结构体
struct sockaddr_in cin = *((struct sockaddr_in*)arg);
//计算客户端地址信息结构体的大小
socklen_t caddrlen = sizeof(cin);
while(1){
bzero(&msg,sizeof(msg));
//获得客户发送的数据包以及发送端地址信息结构体
res = recvfrom(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&cin,&caddrlen);
if(res < 0){
MSG_ERR(recv);
return NULL;
}
//分析数据包协议类型
switch(msg.type){
//登录协议
case 'L':
printf("[%s]已上线\n",msg.name);
//将成功登录的客户端地址信息结构体链接到链表上
list_insert_head(L,cin);
//初始化头指针
LinkList *q = L->next;
while(q != NULL){
//将数据包遍历转发给链表上存在的客户端
if(sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&(q->data),sizeof(q->data)) < 0){
MSG_ERR(sendto);
}
q=q->next;
}
break;
//群聊协议
case 'C':
//因为发送系统消息时通过服务器转发
//所以要判断发送端不为服务器才发送
if(strcmp(msg.name,"*****SYSTEM*****") != 0){
printf("[%s]说:%s\n",msg.name,msg.text);
}
q1 = L->next;
while(q1 != NULL){
//将数据包遍历转发给链表上存在的客户端
if(sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&(q1->data),sizeof(q1->data)) < 0){
MSG_ERR(sendto);
}
q1 = q1->next;
}
break;
//退出协议
case 'Q':
printf("[%s]已退出\n",msg.name);
q2 = L->next;
//定义记录指针用于标记要输出的用户
tempq = NULL;
while(q2->next != NULL){
//利用memset遍历比较用户的完整地址信息结构体,ip,端口
//若一致,则是该用户发送的退出请求,将其删除
if(memcmp(&(q2->next->data),&cin,sizeof(cin)) == 0){
tempq = q2->next;
q2->next = tempq->next;
free(tempq);
}
//若不是,转发退出信息给其他用户
else{
q2=q2->next;
if(sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&q2->data,sizeof(q2->data)) < 0){
MSG_ERR(sendto);
}
}
}
break;
}
}
}
//sendto处理函数
void* funcS(void* arg){
//若沿用msg会导致发送的数据包被recvfrom处理函数一直覆盖
//因此使用新的数据包
struct rule msg_manager;
msg_manager.type = 'C';
strcpy(msg_manager.name,"*****SYSTEM*****");
// struct sockaddr_in sin = *((struct sockaddr_in*)arg);
while(1){
bzero(buf,sizeof(buf));
fgets(buf,128,stdin);
buf[strlen(buf)-1]='\0';
strcpy(msg_manager.text,buf);
//将数据包发给服务器自身,通过服务器转发该数据包给所有用户
res = sendto(sfd,&msg_manager,sizeof(msg_manager),0,(struct sockaddr*)&sein,sizeof(sein));
if(res < 0){
MSG_ERR(send);
return NULL;
}
}
}
//创建链表
LinkList *list_create(){
//申请一个结点的空间
LinkList *L=(LinkList*)malloc(sizeof(LinkList));
if(NULL==L){
printf("申请空间失败\n");
return NULL;
}
//结点初始化
L->len = 0;
L->next = NULL; //悬空结点指针
printf("申请空间成功\n");
//返回链表首地址
return L;
}
//申请结点封装数据函数
LinkList *list_NodeIn(struct sockaddr_in e){
//申请一个结点的空间
LinkList *p = (LinkList*)malloc(sizeof(LinkList));
if(NULL==p){
printf("空间申请失败\n");
return NULL;
}
//结点初始化
p->data=e;
p->next=NULL;
//返回结点地址
return p;
}
//链表 头插入
int list_insert_head(LinkList *L,struct sockaddr_in e){
//判断逻辑
if(NULL==L){
printf("所给链表不合法\n");
return 0;
}
//申请结点封装数据
LinkList *p=list_NodeIn(e);
//头插入逻辑
p->next=L->next; //先令后一节点挂载自己
L->next=p; //再令自己挂载头结点
//表长变化
L->len++;
printf("插入成功\n");
return 1;
}
CliUdpchat:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#define IP "192.168.1.164"
#define PORT 6666
#define MSG_ERR(msg){\
fprintf(stderr,"line : %d\n",__LINE__);\
perror("msg");\
}
//recvfrom处理函数
void* funcR(void* arg);
//sendto处理函数
void* funcS(void* arg);
socklen_t addrlen = 0;
char buf[128] = "";
char myname[32] = "";
ssize_t res = 0;
int sfd = 0;
//数据协议
struct rule{
char type;
char name[32];
char text[128];
}msg;
int main(int argc, const char *argv[])
{
//创建流式套接字
sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd < 0){
MSG_ERR(socket);
return -1;
}
//填充服务器地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(IP);
addrlen = sizeof(sin);
//用于接受服务器转发的信息的线程
pthread_t tidR;
if(pthread_create(&tidR,NULL,funcR,&sin) < 0){
MSG_ERR(pthread_create);
return -1;
}
//发送请求的线程
pthread_t tidS;
if(pthread_create(&tidS,NULL,funcS,&sin) < 0){
MSG_ERR(pthread_create);
return -1;
}
pthread_detach(tidR);
pthread_join(tidS,NULL);
return 0;
}
//recvfrom处理函数
void* funcR(void* arg){
//获得服务器地址信息结构体
struct sockaddr_in sin = *(struct sockaddr_in*)arg;
while(1){
//bzero(&msg,sizeof(msg));
//获得服务器发送的数据包即服务器地址信息结构体
res = recvfrom(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&sin,&addrlen);
if(res < 0){
MSG_ERR(recv);
return NULL;
}
//若收到的数据包是自己发送的,则不打印信息
if(strcmp(msg.name,myname) == 0){
continue;
}
else{
//判断协议类型
switch(msg.type){
case 'L':
printf("[%s]已上线\n",msg.name);
break;
case 'C':
printf("[%s]说:%s\n",msg.name,msg.text);
break;
case 'Q':
printf("[%s]已退出\n",msg.name);
break;
}
}
}
}
//sendto处理函数
void* funcS(void* arg){
//获得服务器地址信息结构体
struct sockaddr_in sin = *(struct sockaddr_in*)arg;
//在发送群聊前发送登录协议
bzero(&msg,sizeof(msg));
msg.type = 'L';
bzero(myname,sizeof(myname));
fgets(myname,sizeof(msg.name),stdin);
myname[strlen(myname)-1] = 0;
strcpy(msg.name,myname);
res = sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&sin,addrlen);
if(res < 0){
MSG_ERR(send);
return NULL;
}
//开始发送群聊与退出请求
while(1){
// bzero(&msg,sizeof(msg));
bzero(buf,sizeof(buf));
fgets(buf,32,stdin);
buf[strlen(buf)-1]='\0';
strcpy(msg.text,buf);
strcpy(msg.name,myname);
//通过判断数据内容确定是群聊还是退出请求
if(strcmp(msg.text,"quit") == 0){
msg.type = 'Q';
res = sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&sin,addrlen);
if(res < 0){
MSG_ERR(send);
return NULL;
}
break;
}
else{
msg.type = 'C';
res = sendto(sfd,&msg,sizeof(msg),0,(struct sockaddr*)&sin,addrlen);
if(res < 0){
MSG_ERR(send);
return NULL;
}
}
}
}