一、功能说明
利用 TCP 套接字实现简单的 QQ 聊天系统。该聊天系统支持新用户注册、用户登录和退出、在线聊天服务、在线用户查询功能。
二、项目框图及说明
如图所示,当用户通过客户端成功建立连接后,服务器使用 pthread_create 创建一个子线程用于与该客户端通信,pthread_create 的第三个参数 Handle 是该项目中集中处理用户操作并给出相关反馈的函数,该子线程执行这个函数。当另一个用户建立连接成功后,再创建一个与之对应的子线程,实现服务器并发, 达到简单在线聊天的目标。
三、项目关键数据结构
在这个项目中,需要将线程 id、通信标识符、客户端地址信息全部传入子线程,因此需要将这些数据打包成结构体 SockInfo,每个线程都对应相关信息, 因此还要定义结构体数组 SockInfo infos[];另外需要存放用户信息的结构体数组 User list[];
四、关键函数说明
void WFile(User t):将用户名写入文件的函数
int IsExist(User u):判断用户是否存在的函数
void *Handle(void *arg):集中处理用户操作并给出相关反馈的函数(在子线程中运行)
五、本项目源码
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_SIZE 100
#define SRVPORT 3000
#define BACKLOG 10
struct SockInfo //信息结构体,存放Handle函数所需要的信息
{
int fd; // 通信
int online;
pthread_t tid; // 线程ID
struct sockaddr_in addr; // 地址信息
char onlineuser[20];
};
struct SockInfo infos[128];
int max = sizeof(infos) / sizeof(infos[0]);
typedef struct
{
char username[10];
char password[10];
} User;
User list[MAX_SIZE]; User u;
char* filename = "user.txt";
int x = 0, y = 0; User lg1[20]; User lg2[20]; char tempname[20]; int count=0;
void WFile(User t) //将用户名写入文件的函数;
{
FILE* fw = fopen(filename, "a+");
fprintf(fw, "%s", t.username);
fprintf(fw, "\t");
fprintf(fw, "%s", t.password);
fprintf(fw, "\n");
}
int IsExist(User u) //判断用户是否存在
{
int i;
for (i = 0; i < MAX_SIZE; i++)
{
if (0 == strcmp(list[i].username, u.username) && 0 == strcmp(list[i].password, u.password))
{
return 1;
}
}
return -1;
}
void *Handle(void *arg) {
char buf[1024];
struct SockInfo* info = (struct SockInfo*)arg;
int r;
r=read(info->fd, buf, 1023);
buf[r]=0;
if (strcmp(buf, "1") == 0) {
printf("客户请求登录\n");
write(info->fd, "请输入用户名:", strlen("请输入用户名:"));
r = read(info->fd, buf, 1023);
buf[r] = 0;
strcpy(lg1[x].username, buf);
strcpy(tempname, buf);
printf("用户名:%s\n", lg1[x].username);
write(info->fd, "请输入密码:", strlen("请输入密码:"));
r = read(info->fd, buf, 1023);
buf[r] = 0;
strcpy(lg1[x].password, buf);
printf("密码:%s\n", lg1[x].password);
if (1 == IsExist(lg1[x]))
{
printf("登录成功\n");
write(info->fd, "登录成功\n", strlen("登录成功\n"));
info->online = 1; strcpy(info->onlineuser, tempname);
x++;
}
else
{
printf("账号或密码错误\n");
write(info->fd, "账号或密码错误\n", strlen("账号或密码错误\n"));
}
}
else if (strcmp(buf, "2") == 0)
{
int a;
printf("客户请求注册\n");
write(info->fd, "请输入用户名:", strlen("请输入用户名:"));
r = read(info->fd, buf, 1023);
buf[r] = 0;
strcpy(lg2[y].username, buf);
printf("%s", buf);
int temp = 0;
for (a = 0; a < MAX_SIZE; a++)
{
if (0 == strcmp(list[a].username, lg2[y].username))
{
write(info->fd, "该账号已注册", strlen("该账号已注册"));
temp = 1;
break;
}
}
if (temp == 0) {
write(info->fd, "账号可注册", strlen("账号可注册"));
write(info->fd, "请输入密码:", strlen("请输入密码:"));
r = read(info->fd, buf, 1023);
buf[r] = 0;
strcpy(lg2[y].password, buf);
WFile(lg2[y]);
strcpy(list[count].username, lg2[y].username);
strcpy(list[count].password, lg2[y].password);
count++;
write(info->fd, "已注册成功", strlen("已注册成功"));
y++;
}
}
char roster[1024];
char chat[1024];
char buff[1024];
int l=0;
char roster1[1024];
while (1) {
strcpy(roster1,"当前在线用户有:");
for (int i = 0; i < max; i++)
{
if (infos[i].online == 1)
{
strcat(roster, infos[i].onlineuser);
strcat(roster, " ");
}
}
strcat(roster1,roster);
write(info->fd, roster1, strlen(roster1));
bzero(roster,strlen(roster));
bzero(roster1,strlen(roster1));
r = read(info->fd, buf, 1023);
buf[r] = 0;
if (strcmp(buf, "#") == 0) {
bzero(info, sizeof(info));
info->fd = -1;
info->tid = -1;
info->online = 0;
bzero(info->onlineuser, strlen(info->onlineuser));
close(info->fd);
pthread_exit(NULL);
}
else {
l = read(info->fd, buff, 1023);
buff[l] = 0;
strcpy(chat, info->onlineuser);
strcat(chat, ":");
strcat(chat, buff);
for (int i = 0; i < max; i++)
{
if (strcmp(buf, infos[i].onlineuser) == 0)
{
write(infos[i].fd, chat, strlen(chat));
}
}
}
}
}
int main()
{
FILE* fp = fopen(filename, "r");
int i = 0;
if (NULL == fp)
{
printf("FILE NOT FOUND");
return -1;
}
char uname[10];
char upassword[10];
for (i = 0; i < MAX_SIZE; i++)
{
fscanf(fp, "%s%s", uname, upassword);
strcpy(list[i].username, uname);
strcpy(list[i].password, upassword);
}
for (i = 0; i < MAX_SIZE; i++)
{
if (strcmp(list[i].username, "") == 0)
count = i;
}
int sockfd;
struct sockaddr_in srvaddr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket creat error");
exit(1);
}
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(SRVPORT);
if (inet_aton("192.168.87.128", &srvaddr.sin_addr) == 0) {
printf("addr convert error\n");
close(sockfd);
exit(1);
}
bzero(&(srvaddr.sin_zero), 8);
if (bind(sockfd, (struct sockaddr*)&srvaddr, sizeof(struct sockaddr)) == -1) {
printf("bind error\n");
close(sockfd);
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
printf("listen error\n");
close(sockfd);
exit(1);
}
int len = sizeof(struct sockaddr);
for (int i = 0; i < max; ++i)
{
bzero(&infos[i], sizeof(infos[i]));
infos[i].fd = -1;
infos[i].tid = -1;
infos[i].online=0;
bzero(infos[i].onlineuser,20);
}
while (1) {
struct SockInfo* pinfo;
for(int i=0; i<max; ++i)
{
if(infos[i].fd == -1)
{
pinfo = &infos[i];
break;
}
if(i == max-1)
{
sleep(1);
i--;
}
}
int connfd = accept(sockfd, (struct sockaddr*)&pinfo->addr, &len);
printf("parent thread, connfd: %d\n", connfd);
if(connfd == -1)
{
perror("accept");
exit(0);
}
pinfo->fd = connfd;
pthread_create(&pinfo->tid, NULL, Handle, pinfo);
pthread_detach(pinfo->tid);
}
close(sockfd);
}
客户端:
#include <stdio.h>
#include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <unistd.h> #define PORT 3000
int main()
{
int sockfd;
char buf[1024];
struct sockaddr_in srvaddr;
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("can't create socket\n");
exit(1);
}
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(PORT);
if(inet_aton("192.168.87.128",&srvaddr.sin_addr)==0){
printf("addr convert error\n");
close(sockfd);
exit(1);
}
if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("connect error\n");
close(sockfd);
exit(1);
}
printf("登录:1 注册:2\n");
bzero(buf,sizeof(buf));
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
if(strcmp(buf,"1")==0)
{
char recvBuff[1024];
int r;
r=read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
r=read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
r=read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
}
if(strcmp(buf,"2")==0)
{
char recvBuff[1024];
int r;
r = read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
r = read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
if(strcmp(recvBuff,"该账号已注册")==0){
return 0;
}
r = read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
r = read(sockfd, recvBuff, 1023);
recvBuff[r] = 0;
printf("%s\n",recvBuff);
return 0;
}
printf("请输入要发送的对象和内容,按“#”退出登录\n");
char buff[1024];
char recvBuff[1024];
char rcv[1024];
int l = 0,h=0;
if (!fork()) {
while (1) {
l = read(sockfd, recvBuff, 1023);
recvBuff[l] = 0;
printf("%s\n", recvBuff);
h = read(sockfd, rcv, 1023);
rcv[h] = 0;
printf("%s\n", rcv);
}
}
while (1) {
bzero(buf,1024);
bzero(buff, 1024);
scanf("%s", buf);
write(sockfd, buf, strlen(buf));
if (strcmp(buf, "#") == 0) {
close(sockfd);
exit(1);
}
scanf("%s", buff);
write(sockfd, buff, strlen(buff));
}
close(sockfd);
return 0;
}
六、项目具体操作说明
实现环境:VMware Workstation 16 Pro Ubuntu 18.04 LTS;
服务器文件:srv.c 客户端文件:clt.c 用户信息文件 user.txt;
首先进入客户端和服务器所在目录,使用命令 gcc -o srv srv.c -lpthread 和gcc -o clt clt.c 编译服务器和客户端文件,生成两个可执行文件 srv 和 clt; 输入./srv 和./clt 执行这两个文件,然后就可以开始聊天。
用户操作流程如下图:
实际操作过程截图: