三. 基于UDP的网络聊天室
项目需求:
- 如果有用户登录,其他用户可以收到这个人的登录信息
- 如果有人发送信息,其他用户可以收到这个人的群聊信息
- 如果有人下线,其他用户可以收到这个人的下线信息
- 服务器可以发送系统信息
linklist.h
#ifndef LINKLIST_H
#define LINKLIST_H
#include <myhead.h>
//定义数据类型
typedef struct datatype
{
char usrName[20]; //用户名
struct sockaddr_in cin;
} datatype, *datatype_ptr;
//定义结点类型
typedef struct Node
{
int len; //头结点数据域
datatype data; //普通结点数据域
struct Node *next; //指针域
} Node, *NodePtr;
//给线程传送消息
struct qtr
{
int sfd;
struct sockaddr_in sin;
NodePtr L;
};
//创建链表
NodePtr list_create();
//申请结点封装数据函数
NodePtr apply_node(datatype e);
//链表判空
int list_empty(NodePtr L);
//头插
int list_insert_head(NodePtr L, datatype e);
//链表遍历函数
int list_show(NodePtr L, char buf[128] );
//通过位置查找结点
NodePtr list_search_pos(NodePtr L, int pos);
//任意位置插入
int list_insert_pos(NodePtr L, int pos, datatype e);
//链表头删
int list_delete_head(NodePtr L);
//链表任意位置删除
int list_delete_pos(NodePtr L, int pos);
//链表按值查找返回位置
int list_search_value(NodePtr L, datatype e);
//链表按位置进行修改
int list_update_pos(NodePtr L, int pos, datatype e);
//按值进行修改
int list_update_value(NodePtr L, datatype old_e, datatype new_e);
//将链表进行翻转
void list_reverse(NodePtr L);
//释放链表
void list_destroy(NodePtr L);
#endif
linklist.c
#include "linklist.h"
//创建链表
NodePtr list_create()
{
//只需要在堆区申请一个头结点
NodePtr L = (NodePtr)malloc(sizeof(Node));
if (NULL == L)
{
printf("创建失败\n");
return NULL;
}
//程序执行至此,说明头结点创建结束
L->len = 0; //表示链表长度为0
L->next = NULL; ///防止野指针
printf("链表创建成功\n");
return L;
}
//申请结点封装数据函数
NodePtr apply_node(datatype e)
{
//在堆区申请一个结点的大小
NodePtr p = (NodePtr)malloc(sizeof(Node));
if (NULL == p)
{
printf("结点申请失败\n");
return NULL;
}
//给结点内容赋值
strncpy(p->data.usrName, e.usrName, sizeof(e.usrName)); // 复制用户名
if (memcpy(&p->data.cin, &e.cin, sizeof(struct sockaddr_in)) == &p->data.cin) {
printf("memcpy successful!\n");
} else {
printf("memcpy failed!\n");
} // 复制sockaddr_in结构体
p->next = NULL; //指针域
return p;
}
//链表判空
int list_empty(NodePtr L)
{
return L->next == NULL;
}
//头插
int list_insert_head(NodePtr L, datatype e)
{
//判断逻辑
if (NULL == L)
{
printf("链表不合法\n");
return -1;
}
//申请结点封装数据
NodePtr p = apply_node(e);
if (NULL == p)
{
return -1;
}
//头插逻辑
p->next = L->next;
L->next = p;
//表的变化
L->len++;
printf("头插成功\n");
return 0;
}
//链表遍历函数
int list_show(NodePtr L,char buf[128])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
//判断逻辑
if (NULL == L || list_empty(L))
{
printf("遍历失败\n");
return -1;
}
//遍历逻辑
NodePtr q = L->next; //定义遍历指针从第一个结点出发
while (q != NULL)
{
//输出数据域
//输出数据域
sendto(sfd, buf, strlen(buf), 0, (struct sockaddr *)&q->data.cin, sizeof(q->data.cin));
q = q->next; //指针向后偏移一个
}
printf("\n");
}
//通过位置查找结点
NodePtr list_search_pos(NodePtr L, int pos)
{
//判断逻辑
if (NULL == L || list_empty(L) || pos < 0 || pos > L->len)
{
printf("查找失败\n");
return NULL;
}
//查找逻辑
//定义遍历指针从头结点出发
NodePtr q = L;
for (int i = 0; i < pos; i++)
{
q = q->next;
}
return q; //将找到的结点地址返回
}
//任意位置插入
int list_insert_pos(NodePtr L, int pos, datatype e)
{
//判断逻辑
if (NULL == L || pos < 1 || pos > L->len + 1)
{
printf("插入失败\n");
return -1;
}
//申请结点封装数据
NodePtr p = apply_node(e);
if (NULL == p)
{
return -1;
}
//调用函数查找前驱结点
NodePtr q = list_search_pos(L, pos - 1);
//插入逻辑
p->next = q->next;
q->next = p;
//表的变化
L->len++;
printf("插入成功\n");
return 0;
}
//链表头删
int list_delete_head(NodePtr L)
{
//判断逻辑
if (NULL == L || list_empty(L))
{
printf("删除失败\n");
return -1;
}
//删除三部曲
NodePtr p = L->next; //标记
L->next = p->next; //L->next->next 孤立
free(p); //释放
p = NULL;
//表长变化
L->len--;
printf("头删成功\n");
return 0;
}
//链表任意位置删除
int list_delete_pos(NodePtr L, int pos)
{
//判断逻辑
if (NULL == L || list_empty(L) || pos < 1 || pos > L->len)
{
printf("删除失败\n");
return -1;
}
//找到前驱结点
NodePtr q = list_search_pos(L, pos - 1);
//删除逻辑
NodePtr p = q->next; //标记
q->next = q->next->next; //p->next 孤立
free(p); //释放
p = NULL;
//表的变化
L->len--;
printf("删除成功\n");
return 0;
}
//链表按值查找返回位置
int list_search_value(NodePtr L, datatype e)
{
//判断逻辑
if (NULL == L || list_empty(L))
{
printf("查找失败\n");
return -1;
}
//查找逻辑
//定义遍历指针从第一个结点出发
NodePtr q = L->next;
for (int index = 1; index <= L->len; index++)
{
//判断当前结点的值是否为要找的数据
if (memcmp(&q->data.cin, &e.cin, sizeof(struct sockaddr_in)) == 0)
{
printf("找到了\n");
return index;
}
q = q->next; //继续向后遍历
}
//程序执行至此,表示没找到
printf("没找到\n");
return -1;
}
//链表按位置进行修改
int list_update_pos(NodePtr L, int pos, datatype e)
{
//判断逻辑
if (NULL == L || list_empty(L) || pos < 1 || pos > L->len)
{
printf("修改失败\n");
return -1;
}
//按位置查找逻辑
NodePtr p = list_search_pos(L, pos);
//修改逻辑
p->data = e;
printf("修改成功\n");
return 0;
}
//按值进行修改
int list_update_value(NodePtr L, datatype old_e, datatype new_e)
{
//判断逻辑
if (NULL == L || list_empty(L))
{
printf("修改失败\n");
return -1;
}
//按值查找位置
int res = list_search_value(L, old_e);
if (res == -1)
{
printf("没有要修改的值\n");
return -1;
}
//按位置修改
list_update_pos(L, res, new_e);
printf("修改成功\n");
return 0;
}
//将链表进行翻转
void list_reverse(NodePtr L)
{
//判断逻辑
if (NULL == L || L->len <= 1)
{
printf("翻转失败\n");
return;
}
//翻转逻辑
NodePtr H = L->next; //将链表元素进行托付
L->next = NULL; //自己白手起家
NodePtr p = NULL; //结点的搬运工
while (H != NULL)
{
p = H; //搬运第一个结点
H = H->next; //头指针后移
//将p以头插的方式放入L中
p->next = L->next;
L->next = p;
}
printf("翻转成功\n");
}
//释放链表
void list_destroy(NodePtr L)
{
//判断逻辑
if (NULL == L)
{
return;
}
//将所有结点进行释放
while (!list_empty(L))
{
//头删
list_delete_head(L);
}
//释放头结点
free(L);
L = NULL;
printf("释放成功\n");
}
UDP_SER.c
#include <myhead.h>
#include "linklist.h"
#define SER_PORT 9999 //服务器端口号
#define SER_IP "192.168.0.108" //服务器ip地址
NodePtr L;
void *ser_end(void *arg)
{
char buf_ser[128] = "";
while (1)
{
fgets(buf_ser, sizeof(buf_ser), stdin);
buf_ser[strlen(buf_ser) - 1] = 0;
if (strcmp(buf_ser, "quit") == 0)
{
exit(0);
break;
}
list_show(((struct qtr *)arg)->L, buf_ser);
}
}
void *read_data(void *arg)
{
//3、数据收发
char buf[128] = "";
struct sockaddr_in cin; //接受对端地址信息
socklen_t addrlen = sizeof(cin); //接受地址长度
while (1)
{
//清空容器
bzero(buf, sizeof(buf));
//从套接字中读取数据
recvfrom(((struct qtr *)arg)->sfd, buf, sizeof(buf), 0, (struct sockaddr *)&cin, &addrlen);
if (strcmp(buf, "connect") == 0)
{
datatype e = {"name", cin};
list_insert_head(((struct qtr *)arg)->L, e);
list_show(((struct qtr *)arg)->L, buf);
}
else if (strcmp(buf, "quit") == 0)
{
snprintf(buf, sizeof(buf), "[%s:%d]: 成功下线", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
list_show(((struct qtr *)arg)->L, buf);
datatype e;
memcpy(&e.cin, &cin, sizeof(struct sockaddr_in)); // 复制sockaddr_in结构体
int pos = list_search_value(((struct qtr *)arg)->L, e);
list_delete_pos(((struct qtr *)arg)->L, pos);
printf("%s\n", buf);
list_show(((struct qtr *)arg)->L, buf);
}
else
{
printf("%s\n", buf);
list_show(((struct qtr *)arg)->L, buf);
}
}
//4、关闭文件描述符
close(((struct qtr *)arg)->sfd);
}
int main(int argc, const char *argv[])
{
L = list_create();
char buf_ser[128] = "";
//1、创建用于通信的服务器套接字文件描述符
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd); //3
//将端口号快速重用
int reuse = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");
//2、为套接字绑定ip地址和端口号
//2.1 填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址
//2.2 绑定工作
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
struct qtr A = {sfd, sin, L};
pthread_t tid;
pthread_create(&tid, NULL, read_data, &A);
pthread_t end_id;
pthread_create(&end_id, NULL, ser_end, &A);
pthread_join(tid, NULL);
return 0;
}
UDP_CLI.c
#include <myhead.h>
#define SER_PORT 9999 //服务器端口号
#define SER_IP "192.168.0.108" //服务器ip地址
#define CLI_PORT 5555 //客户端端口号
#define CLI_IP "192.168.0.108" //客户端ip地址
void *read_data(void *arg)
{
char buf[128];
while (1)
{
//接受服务器发来的消息
recvfrom(*((int *)arg), buf, sizeof(buf), 0, NULL, NULL);
printf("%s\n", buf);
//清空容器
bzero(buf, sizeof(buf));
}
}
int main(int argc, const char *argv[])
{
//1、创建用于通信的服务器套接字文件描述符
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if (cfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", cfd); //3
//将端口号快速重用
int reuse = 1;
if (setsockopt(cfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");
//2、为套接字绑定ip地址和端口号
//2.1 填充地址信息结构体
struct sockaddr_in cin;
cin.sin_family = AF_INET; //通信域
cin.sin_port = htons(CLI_PORT); //端口号
cin.sin_addr.s_addr = inet_addr(CLI_IP); //ip地址
//2.2 绑定工作
if (bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//3、数据收发
char buf[256] = "";
char buf_fgets[128] = "";
//填充服务器的地址信息结构体
struct sockaddr_in sin; //接受对端地址信息
sin.sin_family = AF_INET; //服务器的通信域
sin.sin_port = htons(SER_PORT); //服务器端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //服务器ip地址
//申请连接
strcpy(buf, "connect");
sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));
//接受服务器发来的消息
recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
bzero(buf, sizeof(buf));
//连接成功信号
snprintf(buf, sizeof(buf), "[%s:%d]: 已成功连接\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));
bzero(buf, sizeof(buf));
//接受服务器发来的消息
recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
printf("%s\n", buf);
pthread_t tid;
pthread_create(&tid, NULL, read_data, &cfd);
while (1)
{
bzero(buf, sizeof(buf));
//从终端获取数据
printf("请输入>>>>\n");
fgets(buf_fgets, sizeof(buf_fgets), stdin);
buf_fgets[strlen(buf_fgets) - 1] = 0;
if (strcmp(buf_fgets, "quit") == 0)
{
strcpy(buf, buf_fgets);
//将数据发送给服务器
sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));
break;
}
snprintf(buf, sizeof(buf), "[%s:%d]: ", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
strcat(buf, buf_fgets);
//将数据发送给服务器
sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));
}
//4、关闭文件描述符
close(cfd);
return 0;
}