目录
聊天室的功能:
1.有新用户登录,其他在线的用户可以收到登录信息
2.有用户群聊,其他在线的用户可以收到群聊信息
3.有用户退出,其他在线的用户可以收到退出信息
4.服务器可以发送系统信息
实现上述功能所需知识:
单链表的创建及头插法插入新节点,
链表节点结构体
数据域:存储客户端网络信息结构体
指针域,下个节点的地址
typedef struct node {
struct sockaddr_in c_addr; //数据域,存储用户网络信息结构体
struct node* next; //指针域,
} user_list_t;
自定义应用层协议结构体,用来传输数据
code 操作码:根据操作码的不同,服务器端执行对客户端的数据执行不同的操作
name:客户端用户名
buff:客户端发送的数据
#define N 128
typedef struct c_s {
char code; //操作码
char name[N];
char buff[N];
} msg_t;
服务器程序及客户端程序所使用的函数
系统提供的部分函数
1.socket
int socket(int domain, int type, int protocol);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
功能:创建套接字
参数:
domain:指定通信域
type:套接字类型
protocol:附加协议
返回值:
成功返回所创建的套接字文件都文件描述符
失败返回-1
2.bind
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
功能:将服务器的网络信息结构体和套接字绑定
参数:
sockfd:套接字
addr:网络信息结构体
addrlen: addr的大小
返回值:
成功返回0
失败返回-1
3.listen
int listen(int sockfd, int backlog);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
功能:将套接字设置为被动监听状态
参数:
sockfd:套接字
backlog:允许同时连接到服务器的客户端的个数
返回值:
成功返回0
失败返回-1
4.fork
pid_t fork(void);
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:创建进程
参数:无
返回值:
-1,创建失败
0,子进程
>0,父进程
5.recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
功能:在套接字上接收一条消息
参数:
sockfd:套接字
buf:存储接收数据的缓冲区的首地址
len:想要接收到数据的大小
flags:标志位
src_addr:客户端网络信息结构体,
addrlen:src_addr的大小
返回值:
成功:实际接收数据的字节数
失败:-1
6.sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
功能:向套接字上发送一条消息
参数:
sockfd:套接字
buf:想要发送数据的的首地址
len:发送数据的大小
flags:标志位
dest_addr:接收方的网络信息结构体,
addrlen:dest_addr的大小
返回值:
成功 实际发送的数据的字节数
失败 -1
7.close
int close(int fd);
头文件:
#include <unistd.h>
功能:关闭套接字文件
参数:fd:文件标识符
返回值:
成功0
失败-1
8.inet_addr
in_addr_t inet_addr(const char *cp);
头文件:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
功能:将无符号四字节整形的IP地址转换为网络字节序
参数:无符号四字节整形的IP地址
返回值:
成功:网络字节序IP地址
失败:-1
9.htons
uint16_t htons(uint16_t hostshort);
头文件:
#include <arpa/inet.h>
功能:将主机字节序的端口号转换为网络字节序
参数:hostshort:short类型的无符号整数
返回值:
成功:返回网络字节序的端口号
失败:-1
自定义函数,实现程序功能:
1.创建链表节点函数:
功能:创建链表节点并初始化
参数:无
返回值:
失败,返回NULL
成功,返回创建的节点的首地址
user_list_t* create_linklist(void)
{
//创建链表节点
user_list_t* L = NULL;
if (NULL == (L = (user_list_t*)malloc(sizeof(user_list_t)))) {
perror("carete user_list_t error");
return NULL;
}
//初始化
L->next = NULL;
return L;
}
2.登陆操作函数
功能:执行登陆操作,(将客户端登陆的消息发送给链表中其他的所有节点,并创建新节点,将客户端的信息头插法,添加到链表中)
参数:
sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字
sockfd:服务器的套接字
L:链表头结点指针
msg:客户端发送的消息结构体
返回值:
-1:失败
0:成功
int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
ptemp = ptemp->next;
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
}
//将新用户信息添加到链表中
user_list_t* pnew = create_linklist();
pnew->c_addr = clientaddr;
pnew->next = L->next;
L->next = pnew;
return 0;
}
3.群聊函数
功能:实现聊天室群聊功能,客户端发送消息,服务器将消息转发给其他所有的客户端
参数:
sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字
sockfd:服务器的套接字
L:链表头结点指针
msg:客户端发送的消息结构体
返回值:
-1:失败
0:成功
int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
ptemp = ptemp->next;
if (memcmp(&(clientaddr), (&ptemp->c_addr), sizeof(ptemp->c_addr))) {
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
}
}
return 0;
}
4.退出操作函数
功能:实现客户端退出登陆操作,将客户端退出的消息发送给其他所有客户端,并将退出的客户端的信息,从链表中删除
参数:
sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字
sockfd:服务器的套接字
L:链表头结点指针
msg:客户端发送的消息结构体
返回值:
-1:失败
0:成功
int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
if (memcmp(&(ptemp->next->c_addr), &(clientaddr), sizeof(clientaddr))) {
ptemp = ptemp->next;
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
} else {
user_list_t* Q = ptemp->next;
ptemp->next = Q->next;
free(Q);
Q = NULL;
}
}
}
服务器程序完整代码:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define N 128
typedef struct node {
struct sockaddr_in c_addr; //数据域,存储用户网络信息结构体
struct node* next; //指针域,
} user_list_t;
typedef struct c_s {
char code; //操作码
char name[N];
char buff[N];
} msg_t;
//创建链表节点
user_list_t* create_linklist(void);
int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);
int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);
int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);
int main(int argc, const char* argv[])
{
if (3 != argc) {
perror("open files num error");
exit(-1);
}
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket error");
exit(-1);
}
//服务器网络信息结构体
struct sockaddr_in serveraddr;
//初始化
memset(&serveraddr, 0, sizeof(serveraddr));
//填充
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //网络字节序,四字节整形
serveraddr.sin_port = htons(atoi(argv[2])); //网络字节序端口号,
socklen_t serveraddr_len = sizeof(serveraddr);
//将套接字与网络信息结构体绑定
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
perror("bind error");
exit(-1);
}
//保存客户端网络信息结构体
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
msg_t msg;
//创建父子进程,收发数据
pid_t pid = fork();
if (-1 == pid) {
perror("fork error");
exit(-1);
} else if (0 == pid) {
//子进程接收数据并处理
//创建保存客户端信息的网络信息结构体的链表头节点
user_list_t* L = create_linklist();
while (1) {
memset(&msg, 0, sizeof(msg));
memset(&clientaddr, 0, sizeof(clientaddr));
if (-1 == (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&clientaddr, &clientaddr_len))) {
perror("recvfrom error");
exit(-1);
}
printf("%8s:%s\n", msg.name, msg.buff);
switch (msg.code) {
case 'L':
//操作L,登陆操作,将客户端的网络结构体存储到链表中,并将登陆消息发送给链表所有客户端
do_login(clientaddr, sockfd, L, msg);
break;
case 'C':
do_chat(clientaddr, sockfd, L, msg);
break;
case 'Q':
do_quit(clientaddr, sockfd, L, msg);
break;
}
}
} else if (0 < pid) {
//父进程发送系统消息
//将操作码置为发消息操作
msg.code = 'C';
//将发送的消息的用户名改为服务器
strcpy(msg.name, "server");
while (1) {
fgets(msg.buff, N, stdin);
msg.buff[strlen(msg.buff) - 1] = '\0';
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
perror("sendto error");
exit(-1);
}
}
}
close(sockfd);
return 0;
}
user_list_t* create_linklist(void)
{
//创建链表节点
user_list_t* L = NULL;
if (NULL == (L = (user_list_t*)malloc(sizeof(user_list_t)))) {
perror("carete user_list_t error");
return NULL;
}
//初始化
L->next = NULL;
return L;
}
//登陆操作函数
int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
ptemp = ptemp->next;
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
}
//将新用户信息添加到链表中
user_list_t* pnew = create_linklist();
pnew->c_addr = clientaddr;
pnew->next = L->next;
L->next = pnew;
return 0;
}
//群聊函数
int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
ptemp = ptemp->next;
if (memcmp(&(clientaddr), (&ptemp->c_addr), sizeof(ptemp->c_addr))) {
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
}
}
return 0;
}
//退出操作函数
int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
user_list_t* ptemp = L;
while (NULL != ptemp->next) {
if (memcmp(&(ptemp->next->c_addr), &(clientaddr), sizeof(clientaddr))) {
ptemp = ptemp->next;
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
perror("sendto error\n");
return -1;
}
} else {
user_list_t* Q = ptemp->next;
ptemp->next = Q->next;
free(Q);
Q = NULL;
}
}
}
客户端程序完整代码:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define N 128
typedef struct c_s {
char code; //操作码
char name[N]; //!注:大小必须与服务器保持一致
char buff[N];
} msg_t;
int main(int argc, const char* argv[])
{
if (3 != argc) {
perror("open files num error");
return -1;
}
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket error");
exit(-1);
}
//服务器网络信息结构体
struct sockaddr_in serveraddr;
//初始化
memset(&serveraddr, 0, sizeof(serveraddr));
//填充
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //网络字节序,四字节整形
serveraddr.sin_port = htons(atoi(argv[2])); //网络字节序端口号,
socklen_t serveraddr_len = sizeof(serveraddr);
msg_t msg;
memset(&msg, 0, sizeof(msg));
//输入用户名
printf("input username > ");
fgets(msg.name, N, stdin);
msg.name[strlen(msg.name) - 1] = '\0';
//将用户名发送到服务器完成登陆操作
msg.code = 'L';
strcpy(msg.buff, "加入群聊");
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
perror("sendto error");
exit(-1);
}
printf("%s %s\n", msg.name, msg.buff);
//创建父子进程,收发数据
pid_t pid = fork();
if (-1 == pid) {
perror("fork error");
exit(-1);
} else if (0 == pid) {
//子进程接收数据
//接收系统消息
while (1) {
memset(&msg, 0, sizeof(msg));
if (-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)) {
perror("sendto error");
exit(-1);
}
printf("%8s:[%s]\n", msg.name, msg.buff);
}
}
else if (0 < pid) {
//父进程发送数据,
while (1) {
//将操作码默认设置为发送数据C
msg.code = 'C';
fgets(msg.buff, sizeof(msg.buff), stdin);
msg.buff[strlen(msg.buff) - 1] = '\0';
if (!strcmp(msg.buff, "quit")) {
msg.code = 'Q';
strcpy(msg.buff, "退出群聊");
}
if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
perror("sendto error");
exit(-1);
}
if (!strcmp(msg.buff, "退出群聊")) {
break;
}
}
kill(pid, SIGKILL);
wait(NULL);
close(sockfd);
}
return 0;
}