Linux c 基于UDP实现多人聊天室
1.数据报格式套接字
数据报格式套接字(代码为SOCK_DGRAM),采用此方法计算机只管发送数据而不去验证发送数据的正确性,不论传输是否被接收,数据流是否有丢失,都不再重新发送,特征如下:
1.强调快速传输而非传输顺序;
2.传输的数据可能丢失也可能损毁;
3.限制每次传输的数据大小;
4.数据的发送和接收是并发的。
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
2.采用UDP协议
而数据报套接字采用的是(User Datagram Protocol)协议,本次聊天室采用UDP协议虽然可能会导致数据丢失,但是聊天并不去强调内容的正确性,而应该强调实时性,并且数据丢失只是小概率事件。
3.实现
流程: server 接收来自client的消息并转发给其他client, 并且自身也能发布公告
client 发送消息给server,接收来自server的信息(包括群发内容和公告)
server.c
在服务器定义了两个结构体,INFO,usr_list分别去接受客户端的信息和保存接入客户端的信息。
typedef int protocol; //自己定义的协议 1为用户登录信息 2为普通信息 3为退出信息
typedef struct{
char name[13]; //用户昵称(自定义)
char msg[64]; //正文内容
protocol P; //协议
}INFO;
typedef struct usr_list{
struct sockaddr_in usr_addr; //保存接入用户的信息
struct usr_list *next; //指针
}usr_node,*List;
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
typedef int protocol; //自定义协议 1登录信息 2普通信息 3退出信息
typedef struct sockaddr_in IN;
int len_s = sizeof(struct sockaddr_in);
IN serv_addr,clnt_addr;
typedef struct{
char name[13];
char msg[64];
protocol P;
}INFO;
int len_info = sizeof(INFO);
typedef struct usr_list{
IN usr_addr;
struct usr_list *next;
}usr_node,*List;
List list; //创建用户链表为全局变量,之后利用线程分别实现接收转发消息和server自身发送消息
void init(); //初始化用户链表
void get_len();//得到链表长度(在测试时用,本例子无实际意义)
void insert_node(IN usr_addr); //插入节点
void del_node(IN usr_addr); //删除节点
void send_log(int serv_fd,INFO info,IN clnt_addr); //发送登录信息
void send_msg(int serv_fd,INFO info,IN clnt_addr); //发送普通信息
void send_off(int serv_fd,INFO info,IN clnt_addr); //发送离线信息
void serv_send(int serv_fd,char *buf); //server发送消息
void *thread_handler(void *); //子线程任务,实现接收转发
int main(int argc, char *argv[])
{
if( argc < 3){
printf("input-> %s <ip> <port>n",argv[0]);
exit(0);
}
int ret;
int serv_fd;
pthread_t thread;
init(); //初始化链表
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2])); //端口号作为终端输入的第三个参数
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址作为终端输入的第二个参数
// serv_addr.sin_port = htons(6060);
// serv_addr.sin_addr.s_addr = inet_addr("192.168.17.80");
serv_fd = socket(AF_INET,SOCK_DGRAM,0); //创建套接字(SOCK_DGRAM)
if(serv_fd < 0){
perror("socket");
exit(0);
}
ret = bind(serv_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); //绑定
if(ret < 0){
perror("bind");
exit(0);
}
printf("server open...n");
pthread_create(&thread,NULL,thread_handler,&serv_fd); //创建子线程
while(1){
char buf[64] = {0};
char msg[48] = {0};
strcat(buf,"server announce:");
fgets(msg,48,stdin); //接收server需要公告的信息
strcat(buf,msg); //将信息添加到数组
serv_send(serv_fd,buf); //server群发;
if( strcmp(msg,"exitn") == 0) //server是否主动关闭,与exitn比较时应该fgets会接收n,推荐strncmp
exit(0);
memset(msg,0,48); //清空
memset(buf,0,64); //清空
}
return 0;
}
void *thread_handler(void *arg){
int serv_fd = *(int *)arg;
int ret;
while(1){
INFO info;
//接收来自用户端的信息,保存在info中
ret = recvfrom(serv_fd,&info,len_info,0,(struct sockaddr*)&clnt_addr,&len_s);
printf("recv a message from %s:",info.name);
if(ret < 0){
perror("recvfrom");
exit(0);
}
//根据自定义协议来进行相应操作
switch(info.P){
case 1:{send_log(serv_fd,info,clnt_addr); break;}
case 2:{send_msg(serv_fd,info,clnt_addr); break;}
case 3:{send_off(serv_fd,info,clnt_addr); break;}
}
}
}
void serv_send(int serv_fd,char *buf){
usr_node *p = list->next;
printf("you send a msg to clientsn");
while(p){
//对链表保存的用户依次发送
sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
p = p->next;
}
}
void send_off(int serv_fd,INFO info,IN clnt_addr){
usr_node *p = list->next;
char buf[100] = {0};
strcat(buf,info.name);
strcat(buf,"Offlinen"); //将用户昵称+Offline保存到buf缓冲区进行群发
//buf[strlen(buf)] = '';
printf("%sn","Offline");
while(p){
sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
p = p->next;
}
del_node(clnt_addr); //删除退出用户节点
}
void send_msg(int serv_fd,INFO info,IN clnt_addr){
usr_node *p = list->next;
char buf[100] = {0};
strcat(buf,info.name);
strcat(buf,":");
strcat(buf,info.msg); //将昵称+:+内容保存到缓冲区
buf[strlen(buf)] = ''; //接收的最后一个字符为'n',删除
printf("%s",info.msg);
//群发
while(p){
//判断是不是用户自身
if(p->usr_addr.sin_port != clnt_addr.sin_port || (p->usr_addr.sin_addr.s_addr & clnt_addr.sin_addr.s_addr) != clnt_addr.sin_addr.s_addr)
sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
p = p->next;
}
}
void send_log(int serv_fd,INFO info,IN clnt_addr){
if(list == NULL)
printf("list is nulln");
usr_node *p = list->next;
char buf[30] = {0};
strcat(buf,info.name);
strcat(buf," login...n"); //将昵称+login...保存到缓冲区群发
int i = 1;
printf("%sn","login...");
while(p){
sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
// printf("%dn",i++);
p = p->next;
}
insert_node(clnt_addr); //增加新用户节点
}
//删除节点
void del_node(IN usr_addr){
List p = list->next,s = list; //p为需要删除节点,s为p的前驱节点
int i = 1;
while(p && p->next!= NULL){
if(p->usr_addr.sin_port == usr_addr.sin_port && (p->usr_addr.sin_addr.s_addr & clnt_addr.sin_addr.s_addr) == clnt_addr.sin_addr.s_addr)
break;
p = p->next;
s = s->next;
// printf("%dn",i++);
}
//多余的步骤 只需要s->next = p->next;
if(p->next == NULL){
s->next = NULL;
free(p);
return ;
}
s->next = p->next;
free(p);
}
//尾插法(头插更简单)
void insert_node(IN usr_addr){
usr_node *node,*p = list;
int i = 1;
while( p->next != NULL ){
p = p->next;
// printf("%dn",i++);
}
node = (List)malloc(sizeof(usr_node));
node->usr_addr = usr_addr;
node->next = p->next;
p->next = node;
// get_len();
}
//得到链表长度(去头结点)
void get_len(){
usr_node *p = list->next;
int i = 0;
while(p){
i++;
p = p->next;
}
printf("%dn",i);
}
//初始化
void init(){
list = (List )malloc(sizeof(usr_node));
list->next = NULL;
}
client.c
client使用了一个与server相同结构体INFO用于存储发送的信息
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#define TRUE 1
#define FALSE 0
typedef int Bool;
typedef int protocol;
typedef struct{
char name[13];
char msg[64];
protocol P;
}INFO; //INFO结构体,保存消息内容
//用户输入信息操作
INFO create_msg(INFO info,char *name){
char msg[64]; //缓冲区
fgets(msg,64,stdin);
strcpy(info.name,name); //将用户自定义昵称保存在INFO的name数组中
strcpy(info.msg,msg); //将fgets得到的正文保存在INFO的msg数组中
if(strcmp(msg,"exitn") == 0){
info.P = 3; // 3为退出协议
printf("offlinen");
}else
info.P = 2; // 2为正文
return info;
}
int main(int argc, char *argv[])
{
if( argc < 3){
printf("input-> %s <ip> <port>",argv[0]);
exit(0);
}
int serv_fd;
int ret;
INFO info;
struct sockaddr_in serv_addr,clnt_addr;
int len_s,len_c;
//Bool jug_exit = FALSE;
pid_t pid;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_fd = socket(AF_INET,SOCK_DGRAM,0); //创建套接字(SOCK_DGRAM)
if(serv_fd < 0){
perror("socket");
exit(0);
}
printf("connected...n");
len_s = sizeof(serv_addr);
char name[13];
printf("set your name:");
fgets(name,13,stdin); //设置昵称
name[strlen(name)-1] = '';
strcpy(info.name,name);
info.P = 1; //1 为登录协议
sendto(serv_fd,&info,sizeof(info),0,(struct sockaddr*)&serv_addr,len_s); //将登录信息发送给server
printf("=================communication area=================n");
pid = fork(); //创建两个进程,一个用来发送消息,一个用来接收server和其他client的消息
if(pid < 0){
perror("fork");
exit(0);
}
if(pid == 0){ //接收进程
char is_exit[20];
strcat(is_exit,name);
strcat(is_exit,"Offlinen");//定义判断退出字符串
while(1){
char buf[64];
//接收来自server发送或转发的消息
recvfrom(serv_fd,&buf,sizeof(buf),0,(struct sockaddr*)&serv_addr,&len_s);
if(strcmp(buf,is_exit) == 0) //如果server返回的是与退出字符串相同的字符串则离线
exit(0);
printf("%s",buf); //显示接收到的消息
memset(buf,0,64);
}
}else{ //发送进程
char msg[64] = {0};
while(1){
info = create_msg(info,name); //调用函数,将将要发送的消息保存到info里面;
//发送到server
ret = sendto(serv_fd,&info,sizeof(info),0,(struct sockaddr*)&serv_addr,len_s);
if(ret < 0){
perror("recvfrom");
exit(0);
}
//如果是退出协议,则退出进程
if(info.P == 3){
jug_exit = TRUE;
wait(0);
break;
}
memset(&info,0,sizeof(info));
}
}
return 0;
}
效果展示
基本功能已大致实现,但任需要优化。
总结
整体项目并不复杂,只需要想清楚server和client分别需要做什么事。server为了保存每次连入进来的client信息,使用了链表,对链表的操作就是简单的插入,删除操作,只是在server中对链表的改变时需要使用互斥pv,避免在收发消息时进行链表的增加删除(本次项目并未实现)。这算数最近学习c语言实现的一个稍微综合一点的项目,虽然不多,却也有所收获,便发布一篇帖子去记录,代码中可能有许多不规范的地方,逻辑可能并不严谨,还请各位不吝赐教。