目录
一、运行效果
1、分别编译客户端和服务端代码
gcc client.c -o C -lpthread
gcc server.c -o S -lpthread
2、运行
先运行服务器端,8888为端口号
./S 8888
再运行客户端,这里创建两个客户端,端口号要和服务端的一样
./C 127.0.0.1 8888
可以看到,左下的窗口运之后,就会进入注册界面;而服务器也会提示有客户端的ip连接进来,这个时候再用右边的窗口运行客户端
进入两个主页之后,服务器就会有不同的port对应不同的客户端
3、使用效果
注册
上图可以看到,左下进行了一个注册之后就会提示注册成功,服务器上面也会有记录,下面进行登录
上图左下登录之后会提示登录成功,服务器也会有记录,而左下的窗口只要再按下回车就能进入到功能主页。下面再用左下的窗口注册一个用户2
上图可以看到,用户2注册成功登录之后,进入功能主页的左下用户也会有提示说右下用户上线了,服务器也会记录。下面左下用户进行“公聊”,只要是在线的用户都会收到信息
上图坐下用户输入数字3,选择功能之后,就会提示输入,输入之后按回车,右下用户就出现了信息(蓝色字体),如下
上图可以看到右下用户出现了“公聊信息”蓝色字体,下面用右边的用户对左边用户进行“私聊”
输入4选择功能之后,会询问对谁发信息,接着就输入用户的名字,在输入发送的消息,按下回车就可以发送,如下
可以看到坐下的用户已经收到私聊的信息,这样就不会像公聊那样谁都能看到,输入5可以查看有谁在线,下面先把右下的用户退出
上图可以看到只查询到自己用户1的名字在线,下面用第二个用户查询
上图可以看到,用户2上线之后,查到两个用户在线
二、代码
chat.h
#ifndef __CHAT_H
#define __CHAT_H
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERVER_PORT 8888
#define SIZE 32
/*最大的用户数量*/
#define MAX_USER_NUM 64
/*在线用户*/
struct ONLINE{
int fd; /*描述符,-1:该用户下线 >0:该用户已经登录,对应的套接字*/
int flage; /*注册标志,-1 该条目没有用户信息 1:该条目有用户注册信息*/
char name[SIZE]; /*姓名*/
char password[SIZE]; /*密码*/
};
/*C/S通信的结构体*/
struct protocol{
int cmd; /*命令*/
int state; /*存储命令返回信息*/
char name[SIZE]; /*用户姓名*/
char data[MAX_USER_NUM]; /*数据*/
};
/*cmd(命令类型)*/
#define BROADCAST 0X00000001 /*广播数据*/
#define PRIVATE 0X00000002 /*私聊*/
#define REGISTE 0X00000004 /*注册*/
#define LOGIN 0X00000008 /*登录*/
#define ONLINEUSER 0X00000010 /*显示在线用户*/
#define LOGOUT 0X00000020 /*退出*/
/*return code(服务器处理结果返回值)*/
#define OP_OK 0X80000000 /*操作成功*/
#define ONLINEUSER_OK 0X80000001 /*显示在线用户,未结束*/
#define ONLINEUSER_OVER 0X88888888 /*显示在线用户,已经发送完毕*/
#define NAME_EXIST 0X80000003 /*注册信息,用户名已经存在*/
#define NAME_PWD_NMATCH 0X80000004 /*登录时,输入的用户名密码不对 */
#define USER_LOGED 0X80000005 /*登录时,提示该用户已经在线*/
#define USER_NOT_REGIST 0X80000006 /*登录时,提示用户没有注册*/
#endif
服务端代码
#include "chat.h"
#include <stdio.h>
#include <unistd.h>
struct ONLINE online[MAX_USER_NUM];
/*将用户注册信息存储到在线用户列表中*/
int add_user(int socket_fd, struct protocol *msg) {
int i, index = -1;
char buf[128] = {0};
/*历在线用户列表数组,从第一个开始找没有被占用的位置,将用户的注册信息存储到该位置*/
for (i = 0; i < MAX_USER_NUM; i++) { /*判断当前位置是否已经被占用*/
if (online[i].flage == -1) { /* 标记该位置为已占用*/
online[i].flage = 1;
strcpy(online[i].name, msg->name);
strcpy(online[i].password, msg->data);
printf("regist %s to %d \n", msg->name, i);
index = i;
return index; /* 返回存储用户信息的位置*/
}
}
return index;
}
/*从在线列表中删除
*在聊天室程序中将在线用户列表中指定位置的客户端信息删除,
*并向所有在线客户端发送某用户下线通知
*/
void del_user_online(int index) {
int i;
char buf[128] = {0};
/*通过比较传入的 index 变量与 0 的大小,来判断传入参数是否合法
*如果 index 小于 0,代表传入参数无效,直接返回
*/
if (index < 0)
return;
/*删除在线用户列表中相应位置的客户端信息
*将在线用户列表中指定位置的客户端信息的 fd 字段设置为 -1,表示该客户端已下线
*/
online[index].fd = -1;
/*通知所有客户端,某个用户下线了
*遍历在线用户列表中的每个客户端,fd 不为 -1
*的客户端,向其发送某用户下线的通知消息buf
*/
sprintf(buf, "%s offline\n", online[index].name);
for (i = 0; i < MAX_USER_NUM; i++) {
if (online[i].fd == -1) {
continue;
}
write(online[i].fd, buf, strlen(buf));
}
return;
}
/*向所有在线用户广播消息*/
void broadcast(int index, struct protocol *msg) {
int i;
char buf[128] = {0};
sprintf(buf, "\033[0;34m\t%s say:%s\33[0m\n", online[index].name, msg->data);
for (i = 0; i < MAX_USER_NUM; i++) {
if ((online[i].fd == -1) || (i == index)) /*跳过不在线的和自己*/
{
continue;
}
write(online[i].fd, buf, strlen(buf));
}
}
/*用于判断准备登录的用户是否已经登录,并返回其在列表中的索引*/
int find_dest_user_online(int socket_fd, int *index, struct protocol *msg) {
int i;
/*遍历在线用户列表数组*/
for (i = 0; i < MAX_USER_NUM; i++) {
if (online[i].flage == -1) {
continue;
}
/*如果用户名和密码都匹配*/
if ((strcmp(msg->name, online[i].name) == 0) &&
(strcmp(msg->data, online[i].password) == 0))
{/*不在线的先建立服务器连接*/
if (online[i].fd == -1) {
online[i].fd = socket_fd;
*index = i;
return OP_OK;
} else {/*在线的打印已经登陆在线*/
printf("%s had login\n", online[i].name);
return USER_LOGED;/*用户已经登陆*/
}
}
}/*遍历完所有在线用户仍然没有找到匹配的用户*/
return NAME_PWD_NMATCH;
}
/*查找在线用户列表 online 中指定用户名*/
int find_dest_user(char *name) {
int i;
for (i = 0; i < MAX_USER_NUM; i++) {
if (online[i].flage == -1) {
continue;
}
if (strcmp(name, online[i].name) == 0) {
return i;
}
}
return -1;
}
/*私聊功能*/
void private(int index, struct protocol *msg) {
int dest_index;
char buf[128];
/*查找目标用户在在线用户列表 online 中的索引*/
dest_index = find_dest_user(msg->name);
if (dest_index == -1) {
/*向发送私聊请求的用户发送一条提示消息,告诉其该用户不在线*/
sprintf(buf, "there is no user :%s \n", msg->name);
write(online[index].fd, buf, strlen(buf));
return;
} else {
sprintf(buf, "\033[0;34m\t%s say to %s:%s\33[0m\n", online[index].name,
online[dest_index].name,
msg->data);
write(online[dest_index].fd, buf, strlen(buf));
return;
}
}
/*列出在线用户*/
void list_online_user(int index) {
int i;
struct protocol msg;
for (i = 0; i < MAX_USER_NUM; i++) {
/*如果该套接字已经关闭,则继续下一个循环*/
if (online[i].fd == -1) {
continue;
}
memset(&msg, 0, sizeof(msg));
msg.cmd = ONLINEUSER;
msg.state = ONLINEUSER_OK;
strcpy(msg.name, online[i].name);
printf("list online[%d].name =%s \n",i, msg.name);
/*向客户端发送在线用户结构体的数据*/
write(online[index].fd, &msg, sizeof(msg));
sleep(1);
}
/*表示在线用户列表已经全部发送完毕*/
msg.cmd = ONLINEUSER;
msg.state = ONLINEUSER_OVER;
/*用于通知客户端当前任务已完成,并结束本次传输*/
write(online[index].fd, &msg, sizeof(msg));
}
/*注册*/
void registe(int socket_fd, int *index, struct protocol *msg) {
int dest_index;
char buf[128];
struct protocol msg_back;
msg_back.cmd = REGISTE;
/*查找该用户名是否已经被其他用户注册*/
dest_index = find_dest_user(msg->name);
if (dest_index == -1) {
*index = add_user(socket_fd, msg);
online[*index].flage = 1;
msg_back.state = OP_OK;
printf("user %s regist success!\n", msg->name);
write(socket_fd, &msg_back, sizeof(msg_back));
return;
} else {/*用户名已存在*/
msg_back.state = NAME_EXIST;
printf("user %s exist!\n", msg->name);
write(socket_fd, &msg_back, sizeof(msg_back));
return;
}
}
/*登录*/
void login(int socket_fd, int *index, struct protocol *msg) {
int i, ret;
char buf[128];
struct protocol msg_back;
msg_back.cmd = LOGIN;
/*查找该用户名是否已经在线*/
ret = find_dest_user_online(socket_fd, index, msg);
if (ret != OP_OK) {/*不等于表示该用户名不存在或者该用户未登录*/
msg_back.state = ret;
strcpy(buf, " no this user\n");
printf("user %s login fail!\n", msg->name);
write(socket_fd, &msg_back, sizeof(msg_back));
return;
} else {/*登录成功*/
msg_back.state = OP_OK;
strcpy(msg_back.data, "login success\n");
printf("user %s login success!index =%d \n", msg->name, *index);
write(online[*index].fd, &msg_back, sizeof(msg_back));
}
// 通知所有客户端,某个用户上线了
sprintf(buf, "%s online\n", online[*index].name);
for (i = 0; i < MAX_USER_NUM; i++) {
if (online[i].fd != -1) {
write(online[i].fd, buf, strlen(buf));
}
}
}
void *func(void *arg) {
int socket_fd = *((int *)arg);
char buf[64];
int len;
int index = -1;
struct protocol msg;
free(arg);
/*进入聊天*/
while (1) {
/*read()函数和recv()函数都可以用于从套接字中读取数据,都会阻塞
*小于等于0说明未能成功读取任何数据或连接已关闭,因此用户被视为离线
*/
if ((len = read(socket_fd, &msg, sizeof(msg))) <= 0) {
printf("%s offline\n", online[index].name);
del_user_online(index);
close(socket_fd);
return;
}
switch (msg.cmd) {
case REGISTE:
registe(socket_fd, &index, &msg);
break;
case LOGIN:
login(socket_fd, &index, &msg);
break;
case BROADCAST:
broadcast(index, &msg);
break;
case PRIVATE:
private(index, &msg);
break;
case ONLINEUSER:
list_online_user(index);
break;
default:
break;
}
}
}
int main(int argc, char *argv[]) {
int ls_fd, new_fd;
int addr_len, clientaddr_len;
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
char buf[64] = {0};
pthread_t pid;
int *arg, i, port_number, ret;
/*判断输入命令行参数数量是否正确*/
if (argc != 2) {
fprintf(stderr, "Usage: %s [port_number]>5000\a\n", argv[0]);
exit(-1);
}
/*将第二个参数转换为整数类型并赋值给变量,然后判断输入的端口参数是否小于0*/
if ((port_number = atoi(argv[1])) < 5000) {
fprintf(stderr, "Usage: %s [port_number]>5000\a\n", argv[0]);
exit(-1);
}
/*原型:int socket(int domain, int type, int protocol);
*创建一个网络通信端点(打开一个网络通信),成功则返回一个网络文件描述符;调用失败,则会返回-1
*domain:AF_INET,协议族名字,代表IPv4互联网协议;如果用IPV6在后面加个6即可
*type:SOCK_STREAM,这是用于 TCP 协议
*protocol:0,表示为给定的通信域和套接字类型选择默认协议
*/
if ((ls_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket() fail\n");
return;
}
/*将 server_addr 结构体变量所占用的内存区域全部清零。*/
memset(&my_addr, 0, sizeof(struct sockaddr_in));
/*指定套接字服务器的地址信息*/
/*表示使用 IPv4 地址*/
my_addr.sin_family = AF_INET;
/*端口号设置并将主机字节顺序转换为网络字节顺序*/
my_addr.sin_port = htons(port_number);
/*使用了宏 INADDR_ANY
* 来指定监听所有可用的网络接口,从而使得服务器能够接受到所有网络接口上的连接请求*/
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr_len = sizeof(struct sockaddr_in);
/*int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
*将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回
*0,失败情况下返回-1 sockfd:网络文件描述符 addr:服务器结构体地址
*addrlen:addr参数所指向的结构体对应的字节长度
*/
if (bind(ls_fd, (struct sockaddr *)(&my_addr), addr_len) == -1) {
perror("bind() fail\n");
exit(-1);
}
/*int listen(int sockfd, int backlog);
*让服务器进程进入监听状态,监听sockfd描述符并创建一个等待连接队列,若执行成功则返回0,否则返回-1
*sockfd:要设置为监听状态的套接字描述符
*backlog:5,指定允许的连接数为
*5。如果有更多的客户端连接请求到达,它们将被服务器拒绝或忽略。
*/
if (listen(ls_fd, 5) == -1) {
fprintf(stderr, "listen error:%s\n\a", strerror(errno));
return;
}
clientaddr_len = sizeof(struct sockaddr_in);
/*初始化online结构体*/
for (i = 0; i < 64; i++) {
online[i].fd = -1;
online[i].flage = -1;
}
while (1) {
/*int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
*调用成功返回一个新的
*socket_fd描述符,用于与客户端通信的。如果发生错误或者连接被中断,则函数返回
*-1 函数成功返回时,addr 所指的缓冲区将被填充上客户端的地址信息,而 addrlen
*则会被更新为实际的地址信息长度
*调用时没有客户端请求连接(等待连接队列中也没有等待连接的请求),
*accept()会进入阻塞状态,直到客户程序建立连接
*/
if ((new_fd = accept(ls_fd, (struct sockaddr *)(&client_addr),
&clientaddr_len)) == -1) {
fprintf(stderr, "Accept error:%s\n\a", strerror(errno));
return;
}
/*char *inet_ntoa(struct in_addr in);
*将32位IP地址转换为点分十进制形式的字符串表示
*/
printf("Client-ip: %s\tport: %d\t\n", inet_ntoa(client_addr.sin_addr),
client_addr.sin_port);
/*用堆空间单独存放new_fd,防止高并发状态覆盖原栈空间的new_fd*/
arg = malloc(sizeof(int));
*arg = new_fd;
/*pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
*创建一个新的线程,返回 0
表示成功,否则返回一个非零的错误码,表示创建线程失败 *thread:线程标识符pid
*attr:指定线程的属性,如果传递了 NULL,则表示使用默认属性
*start_routine:是一个函数指针,用于指定线程启动时要执行的函数
*arg:传递给该函数的参数
*/
if ((ret = pthread_create(&pid, NULL, func, (void *)arg)) != 0) {
perror("pthread_create err");
return;
}
}
close(new_fd);
close(ls_fd);
exit(0);
}
客户端代码
#include "chat.h"
#include <bits/types/timer_t.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int socket_fd, addrlen;
struct sockaddr_in server_addr;
pthread_t pid;
int login_f = -1;
void *func(void *arg) {
int len;
char buf[128] = {0};
struct protocol *msg;
while (1) {
if (login_f != 1) {
continue;
}
memset(buf, 0, sizeof(buf));
if ((len = read(socket_fd, buf, sizeof(buf))) <= 0) {
close(socket_fd);
return;
}
msg = (struct protocol *)buf;
/*显示在线用户*/
if ((msg->state == ONLINEUSER_OK) && (msg->cmd == ONLINEUSER)) {
printf("%s\t", msg->name);
continue;
}
if ((msg->state == ONLINEUSER_OVER) && (msg->cmd == ONLINEUSER)) {
printf("\n");
continue;
}
buf[len] = '\0';
printf("%s\n", buf);
}
}
/*广播信息*/
void broadcast(int fd) {
struct protocol msg;
msg.cmd = BROADCAST;
printf("say:\n#");
scanf("%s", msg.data);
write(fd, &msg, sizeof(msg));
}
/*私聊信息*/
void private(int fd) {
struct protocol msg, msgback;
msg.cmd = PRIVATE;
printf("input name you want to talk:\n\t#");
scanf("%s", msg.name);
printf("\n#say:\n");
scanf("%s", msg.data);
write(fd, &msg, sizeof(msg));
}
/*显示在线人数*/
void list_online_user(int socket_fd) {
struct protocol msg;
msg.cmd = ONLINEUSER;
write(socket_fd, &msg, sizeof(msg));
while (1) {
read(socket_fd, &msg, sizeof(msg));
if (msg.state != ONLINEUSER_OK) {
getchar();
getchar();
break;
} else
printf("%s\r\n", msg.name);
}
}
/*注册*/
int registe(int fd) {
struct protocol msg, msgback;
msg.cmd = REGISTE;
printf("input your name\n");
scanf("%s", msg.name);
printf("input your passwd\n");
scanf("%s", msg.data);
write(socket_fd, &msg, sizeof(msg));
read(socket_fd, &msgback, sizeof(msgback));
if (msgback.state != OP_OK) {
printf("\033[0;31m\tName had exist,try again!\n");
getchar();
getchar();
return -1;
} else {
printf("\033[0;31m\tRegist success!\n");
getchar();
getchar();
return 0;
}
}
/*登陆*/
int login(int fd) {
struct protocol msg, msgback;
msg.cmd = LOGIN;
printf("input your name\n");
scanf("%s", msg.name);
printf("input your passwd\n");
scanf("%s", msg.data);
write(socket_fd, &msg, sizeof(msg));
read(socket_fd, &msgback, sizeof(msgback));
if (msgback.state != OP_OK) {
printf("\033[0;31m\tName had exist,maybe the password is wrong,try "
"again!\33[0m\n");
getchar();
getchar();
login_f = -1;
return NAME_PWD_NMATCH;
} else {
printf("\033[0;31m\tLogin success!\33[0m\n");
getchar();
getchar();
login_f = 1;
return OP_OK;
}
}
/*退出*/
int logout(int fd) {
close(fd);
login_f = -1;
}
int main(int argc, char *argv[]) {
int sel, ret;
int port_number;
int min_sel, max_sel;
struct protocol msg;
/* 检测参数个数*/
if (argc != 3) {
fprintf(stderr, "Usage:%s hostname portnumber\a\n", argv[0]);
exit(-1);
}
/*argv2 存放的是端口号 ,读取该端口,转换成整型变量*/
if ((port_number = atoi(argv[2])) < 5000) {
fprintf(stderr, "Usage:%s hostname [portnumber]>5000\a\n", argv[0]);
exit(-1);
}
/*创建一个 套接字*/
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
fprintf(stderr, "Socket Error:%s\a\n", strerror(errno));
return;
}
/*填充结构体,ip和port必须是服务器的*/
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
/*转换为网络字节序*/
server_addr.sin_port = htons(port_number);
/*点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址*/
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
addrlen = sizeof(struct sockaddr_in);
if (connect(socket_fd, (struct sockaddr *)(&server_addr), addrlen) == -1) {
fprintf(stderr, "Connect Error:%s\a\n", strerror(errno));
return;
}
/*创建线程*/
if ((ret = pthread_create(&pid, NULL, func, NULL)) != 0) {
perror("pthread_create err");
return;
}
while (1) {
system("clear");
if (login_f == -1) {
printf(
"\033[0;33m+---------------------------------------------+\033[0m\n");
printf("\33[0;31m\t 1、 注册 \n\33[0m");
printf("\33[0;31m\t 2、 登录 \33[0m\n");
} else if (login_f == 1) {
printf(
"\033[0;33m+---------------------------------------------+\033[0m\n");
printf("\33[0;31m\t 3、 公聊\33[0m\n");
printf("\33[0;31m\t 4、 私聊\33[0m\n");
printf("\33[0;31m\t 5、 在线用户\33[0m\n");
}
printf("\33[0;31m\t 0、 退出\033[0m\n");
printf(
"\033[0;33m+---------------------------------------------+\033[0m\n");
fflush(stdin);
scanf("%d", &sel);
if (sel == 0) {
break;
}
if (login_f == 1) {
min_sel = 3;
max_sel = 5;
} else if (login_f == -1) {
min_sel = 1;
max_sel = 2;
}
if (sel < min_sel || sel > max_sel) {
printf("输入的数字不在范围内,请重新输入\n");
continue;
}
switch (sel) {
case 1:
registe(socket_fd);
break;
case 2:
login(socket_fd);
break;
case 3:
broadcast(socket_fd);
break;
case 4:
private
(socket_fd);
break;
case 5:
list_online_user(socket_fd);
break;
case 0:
logout(socket_fd);
break;
default:
break;
}
if (sel == 0) {
exit(0);
}
}
}
本项目学习于从0实现基于Linux socket聊天室-多线程服务器模型-1_一口Linux的博客-CSDN博客
从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2_一口Linux的博客-CSDN博客从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2_一口Linux的博客-CSDN博客
从0实现基于Linux socket聊天室-实现聊天室的公聊、私聊功能-4_一口Linux的博客-CSDN博客
一口Linux博主实力强悍、经验丰富、知识领域广,笔下文章内容丰富,大家可以点点关注