[C]基於UDP的局域網多人聊天室的簡單代碼 ..編程小白,代碼可能比較冗長,請各位大佬指出不足 [*・ω・]
運行現象
1.當客戶端(client)打開,輸入暱稱後服務器(server)端以及客戶端的現象
2.當客戶端發送消息,以及服務端發送消息,以及客戶端退出時現象
思路
1.因爲服務端和客戶端都需要具備接收數據和發送數據的能力,因此需要使用多個線程分別完成接收和發送的操作(相關函數pthread_create())
2.因爲需要通過服務端給每個用戶羣發消息,所以用戶的信息需要儲存,用數組和鏈表都可以,這裏本人用的是鏈表
3.因爲UDP協議發送信息時用的是sedto()函數,每次發送數據都需要提供發送目標的ip和端口號,因此我們需要定義一個結構體來存放每個登錄用戶的端口號和ip號等信息.
程序源碼(LINUX)
1.head.h(頭文件)
#ifndef _HEAD_H_
#define _HEAD_H_
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <linux/in.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
//發送信息的標誌變量
#define LOGIN 0
#define QUIT 1
#define SESSION 2
#define SERVER 3
#define RENAME 4
//存放客戶端發送信息
typedef struct msg{
int flag;
char name[16];
char info[64];
}msg;
//用來保存單個用戶的ip,端口號等信息
typedef struct usermsg{
char user_ip[32];
unsigned short user_port;
char user_name[16];
}usermsg;
//存放所有用戶信息的鏈
typedef struct userlink{
usermsg user;
struct userlink *next;
}userlink;
int link_creat(userlink **); //創建鏈表
int link_add(userlink *,usermsg *,int); //添加用戶
int myperror(int,char*); //報錯信息(寫到後面忘了這個函數了,後面寫的函數幾乎都沒有做判斷...)
int link_delete(userlink*,usermsg*,int);//用戶退出,刪除退出用戶的信息
int mass(userlink*,usermsg*,msg*,int); //服務器給各個客戶端羣發信息的函數
void* client_thread(void*); //客戶端thread_create()創建線程函數的最後一個參數,該線程用於接收來自服務器的消息
userlink* link_select(userlink*,usermsg*);//查找指定用戶的函數
void* clientmsg_thread(void *); //服務端thread_create()創建線程函數的最後一個參數,該線程用於給客戶端發送服務端的消息
void* servermsg_thread(void *); //服務端thread_create()創建線程函數的最後一個參數,該線程用於給客戶端發送其他客戶端的消息
#endif
2.datalink.c (存放着各種自定義函數接口)
#include"head.h"
//自定義的報錯函數
int myperror(int flag,char *name){
if(flag < 0){
printf(">>server : %s is error",name);
perror(":");
exit(-1);
return 0;
}else{
printf(">>server : %s is succeed\n",name);
return 0;
}
}
//創建鏈表
int link_creat(userlink **p){
if((*p) == NULL){
(*p) = (userlink*)malloc(sizeof(userlink));
(*p) -> next = NULL;
(*p) -> user.user_port = 0;
return 0;
}
puts("==SERVER== :link is exit");
return 0;
}
//添加用戶,並給所有人發送登錄信息
int link_add(userlink *p,usermsg *umsg,int sockfd){
if(p){
userlink *new = (userlink*)malloc(sizeof(userlink));
new -> next = p -> next;
p -> next = new;
p -> user.user_port ++;
strcpy(new -> user.user_ip,umsg -> user_ip);
new -> user.user_port = umsg -> user_port;
strcpy(new -> user.user_name,umsg->user_name);
//在服務端打印登入用戶的名字,ip,以及端口號
printf(">>server : %s is login,--[ip : %s][port : %u]--\n",umsg -> user_name,
umsg -> user_ip,umsg -> user_port);
new = NULL;
//定義一個新的數據包
msg smsg;
//給數據包定義一個flag標誌位,然後往其中填充一條"xxx"用戶登錄的消息
smsg.flag = SERVER;
memset(smsg.info,'\0',sizeof(smsg.info));
sprintf(smsg.info,">> %s << is login",umsg -> user_name);
//使用mass()羣發函數,發給所有用戶(該函數在下方定義)
mass(p,NULL,&smsg,sockfd);
return 0;
}else{
puts("==SERVER== : link not exist");
return 0;
}
}
//尋找用戶的前一個用戶
userlink* link_select(userlink *p,usermsg *umsg){
while(p -> next){
if(!strcmp(p->next->user.user_ip,umsg->user_ip)&&p->next->user.user_port == umsg->user_port ){
return p;
break;
}
p = p -> next;
}
return NULL;
}
//用戶退出
int link_delete(userlink *head,usermsg *umsg,int sockfd){
userlink *temp,*p = NULL;
if(head){
if((p = link_select(head,umsg))){
temp = p -> next;
p -> next = temp -> next;
//在服務端打印一下退出用戶的名字
printf(">>>server : user[ %s ] will be quit\n",temp ->user.user_name);
//定義一個新的數據包,操作和上文中的登錄操作類似,將數據包填充後調用mass函數羣發
msg bmsg;
bmsg.flag = QUIT;
strcpy(bmsg.name,temp -> user.user_name);
mass(head,umsg,&bmsg,sockfd);
free(temp);
temp = NULL;
head -> user.user_port--;
}else{
puts("==SERVER== : user not exist");
return 0;
}
}else{
puts("==SERVER== : link not exist");
return 0;
}
}
//給所有用戶轉發消息
int mass(userlink *p,usermsg *umsg,msg *bmsg,int sockfd){
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
//當該信息來自客戶端時
if(bmsg->flag == SESSION){
userlink *namep = NULL;
if((namep = link_select(p,umsg))){
strcpy(bmsg->name,namep->next->user.user_name);
}else{
puts("==SERVER== : error,the user is exit???");
}
//當該信息來自服務端時
}else if(bmsg -> flag == SERVER){
strcpy(bmsg->name,"server");
//當該信息是客戶端的退出指令時
}else if(bmsg -> flag == QUIT){
memset(bmsg->info,'\0',strlen(bmsg->info));
sprintf(bmsg->info,"[%s is quit]",bmsg->name);
strcpy(bmsg->name,"server");
}
//在服務端打印一下這條信息的內容
printf(">>> [%s] : %s\n",bmsg->name,bmsg->info);
p = p->next;
//遍歷整個鏈表,將打包好的信息發送給各個客戶端
while(p){
clientaddr.sin_family = AF_INET;
clientaddr.sin_port = htons(p->user.user_port);
clientaddr.sin_addr.s_addr = inet_addr(p->user.user_ip);
sendto(sockfd,bmsg,sizeof(msg),0,(struct sockaddr*)&clientaddr,clientlen);
p = p-> next;
}
}
3.server.c(服務端)
#include"head.h"
//線程信號量
sem_t sem_server;
//線程從該結構體中拿數據
struct thread_pass{
int sockfd;
userlink *head;
msg *bmsg;
usermsg *umsg;
}pass;
//同上,也是用來給線程拿數據的
struct buf{
int sockfd;
userlink *head;
}bufpack;
int main(int argc, const char *argv[])
{
//創建鏈表
userlink *head = NULL;
link_creat(&head);
//生成套接字文件描述符
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
myperror(sockfd,"socket");
struct sockaddr_in serveraddr;
//服務端的ip,協議族,端口號等信息
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
myperror(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)),"bind");
//用來接收客戶端的ip,端口號信息的結構體
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
//自定義的存放客戶端信息的結構體,便於往鏈表中填充
usermsg umsg;
//存放正文信息以及信息標誌符的結構體數據包
msg bmsg,smsg;
pthread_t tid_send_clientmsg,tid_send_servermsg;
bufpack.head = head;
bufpack.sockfd = sockfd;
//創建兩個線程
pthread_create(&tid_send_servermsg,NULL,servermsg_thread,NULL);
pthread_create(&tid_send_clientmsg,NULL,clientmsg_thread,NULL);
//線程分離
pthread_detach(tid_send_clientmsg);
pthread_detach(tid_send_clientmsg);
//初始化線程信號量
sem_init(&sem_server,0,0);
//循環接收數據
while(1){
recvfrom(sockfd,&bmsg,sizeof(bmsg),0,(struct sockaddr*)&clientaddr,&clientlen);
//將存放在clientaddr中的客戶端ip和端口號存放在我們自定義的umsg結構體裏
strcpy(umsg.user_ip,(char*)inet_ntoa(clientaddr.sin_addr.s_addr));
umsg.user_port = ntohs(clientaddr.sin_port);
switch(bmsg.flag){
//當信號量爲LOGIN時,爲用戶登錄,添加鏈表
case LOGIN:
strcpy(umsg.user_name,bmsg.info);
link_add(head,&umsg,sockfd);
break;
//爲QUIT爲用戶退出,刪除鏈表中該用戶的信息
case QUIT:
link_delete(head,&umsg,sockfd);
break;
//SESSION爲用戶發送過來的正文消息,釋放一個信號量,交給線程處理
case SESSION:
pass.head = head;
pass.bmsg = &bmsg;
pass.umsg = &umsg;
pass.sockfd = sockfd;
sem_post(&sem_server);
}
}
return 0;
}
//用來羣發服務端消息的線程
void* servermsg_thread(void *p){
msg smsg;
smsg.flag = SERVER;
while(1){
//fgets阻塞,從終端的輸入緩存區獲取信息,然後發送,沒有信息則阻塞
fgets(smsg.info,sizeof(smsg.info),stdin);
(smsg.info)[strlen(smsg.info) - 1] = '\0';
mass(bufpack.head,NULL,&smsg,bufpack.sockfd);
}
return 0;
}
//用啦羣發客戶端消息的線程
void* clientmsg_thread(void *p){
while(1){
//當只有主線程中信號量釋放時纔會向下運行,否則阻塞
sem_wait(&sem_server);
mass(pass.head,pass.umsg,pass.bmsg,pass.sockfd);
}
}
4.client.c(客戶端)
#include"head.h"
int main(int argc, const char *argv[])
{
//創建套接字文件描述符
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
myperror(sockfd,"socket");
struct sockaddr_in serveraddr;
pthread_t tid;
msg bmsg;
//服務端的各種信息,用於後面sendto()函數
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//輸入名字的提示語句
printf(">>>name (Less than 15 character) : ");
fgets(bmsg.info,16,stdin);
(bmsg.info)[strlen(bmsg.info) - 1] = '\0';
bmsg.flag = LOGIN;
sendto(sockfd,&bmsg,sizeof(bmsg),0,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
//創建線程
pthread_create(&tid,NULL,client_thread,&sockfd);
pthread_detach(tid);
while(1){
//從終端輸入緩存區獲取信息,沒有則阻塞
fgets(bmsg.info,sizeof(bmsg.info),stdin);
(bmsg.info)[strlen(bmsg.info) - 1] = '\0';
//當輸入#quit ---這一退出指令時執行的語句,將標誌位賦值爲QUIT
if(!strcmp(bmsg.info,"#quit")){
bmsg.flag = QUIT;
sendto(sockfd,&bmsg,sizeof(bmsg),0,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
sleep(1);
pthread_cancel(tid);
break;
}else{
bmsg.flag = SESSION;
}
sendto(sockfd,&bmsg,sizeof(bmsg),0,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
}
return 0;
}
//用於接收服務端發來的消息,並打印在終端上
void* client_thread(void *p){
msg recvmsg;
while(1){
recvfrom(*(int *)p,&recvmsg,sizeof(recvmsg),0,NULL,NULL);
printf(">>> [%s] : %s\n",recvmsg.name,recvmsg.info);
}
}
5.Makefile()
.PHONY:all
all:client server
OBJS1=client.o datalink.o
OBJS2=server.o datalink.o
CC=gcc
CFLAGS=-g
client:$(OBJS1)
$(CC) $(CFLAGS) $^ -o $@ -lpthread
server:$(OBJS2)
$(CC) $(CFLAGS) $^ -o $@ -lpthread
.PHONY:clean
clean:
rm $(OBJS)
代碼中的不足之處
1.沒有輸入字符溢出時的優化語句
2.用戶重名無法判斷
3.服務端沒有正常退出的語句
4.代碼冗長,定義的變量以及結構體太多了,不方便閱讀
源碼文件鏈接
廢話
本人爲剛學嵌入式不久的編程小白,目前只學習了一些C語言,文件IO和TCP、UDP協議的一些基礎知識,代碼寫的很爛,非常渴望各位大佬批評以及指點。
最後,非常感謝各位大佬點開這篇帖子,你們的點擊也一定是我學習的動力,感謝✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧