对于初学Linux的开发者而言,聊天室是个较好上手的练习小项目,本次实现简易群聊、聊天内容保存等功能,后面有时间再更新私聊、加好友.....,主要使用线程、TCP、多路复用实现
服务器代码:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LISTENQ 100 //最大监听队列
#define PORT 6000 //监听端口
#define MAXFD 100 //最大的在线用户数量
#define NAMESIZE 20 //客户端姓名占用的字节数
#define TIMESIZE 30 //收到消息时的时间占用的字节数
#define SENDBUFSIZE 1024 //消息的数组长度
#define FILENAMESIZE 40 //聊天记录文件名的长度
#define NEWCLIENTINFORMSIZE 70 //通知新用户上线的消息数组长度
int sockfd; //绑定服务器端的ip地址和端口的套接字
FILE* fp; //文件指针,用于指向保存聊天记录的文件
int maxi = 0; //maxi表示当前client数组中最大的用户的i值
static int client[MAXFD]; //全局数组,保存所有客户端对应的套接字描述符
void tcp_init(int argc, char* argv[])
{
struct sockaddr_in server_addr;
if (argc != 1)
{
fprintf(stderr, "Usage:%s portnumber\a\n", argv[0]);
exit(1);
}
/* 服务器端开始建立 socket 描述符 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Socket error:%s\n\a", strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr 结构 */
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(int));
/* 捆绑 sockfd 描述符 */
if (bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) == -1)
{
fprintf(stderr, "Bind error:%s\n\a", strerror(errno));
exit(1);
}
printf("Server listening port=%d...\n", PORT);
/* 监听 sockfd 描述符 */
if (listen(sockfd, LISTENQ) == -1)
{
fprintf(stderr, "Listen error:%s\n\a", strerror(errno));
exit(1);
}
}
/*接收对应的客户端的消息,并转发给所有在线客户端*/
void recv_and_send_to_all_client(int index)
{
int nbytes = 0;
char sendbuf[SENDBUFSIZE] = { 0 };
int outindex = 0;
time_t tt;
struct tm* pTm;
nbytes = 0;
nbytes = read(client[index], sendbuf, sizeof(sendbuf));
sendbuf[0] = 'M'; //'M'表示正常聊天信息
if (nbytes > 0)
{
tt = time(NULL);
pTm = localtime(&tt);
sprintf(sendbuf + NAMESIZE, " %d-%d-%d %0d:%0d:%0d\n",
(1900 + pTm->tm_year), (1 + pTm->tm_mon), (pTm->tm_mday),
(pTm->tm_hour), (pTm->tm_min), (pTm->tm_sec));
outindex = 0;
/*以下循环将消息转发给所有客户端*/
while (outindex < maxi)
{
if (write(client[outindex], sendbuf, sizeof(sendbuf)) == -1)
{
fprintf(stderr, "Write Error:%s\n", strerror(errno));
close(sockfd);
fclose(fp);
exit(1);
}
outindex++;
}
/*如果某个客户端下线,则删除全局数组client中对应的套接字*/
if (strcmp(sendbuf + NAMESIZE + TIMESIZE, "QUIT") == 0 ||
strcmp(sendbuf + NAMESIZE + TIMESIZE, "Quit") == 0 ||
strcmp(sendbuf + NAMESIZE + TIMESIZE, "quit") == 0)
{
for (; index < maxi - 1; index++)
{
client[index] = client[index + 1];
}
maxi--;
}
/*服务器打印出消息内容并保存消息到文件*/
printf("%s", sendbuf + 1);
printf("%s", sendbuf + NAMESIZE);
printf(" %s\n", sendbuf + NAMESIZE + TIMESIZE);
fprintf(fp, "%s", sendbuf + 1);
fprintf(fp, "%s", sendbuf + NAMESIZE);
fprintf(fp, "%s\n", sendbuf + NAMESIZE + TIMESIZE);
}
}
//监听转发线程入口函数
void* check_client_send(void* arg)
{
static int index = 0;//very important
int select_n = 0;
int in = 0;
fd_set allset, rset;
FD_ZERO(&allset);
while (1)
{
if (maxi > 0)
{
for (index = 0; index < maxi; index++)
{
FD_SET(client[index], &allset);
}
rset = allset;
//阻塞监听所有的客户端对应的套接字有没有可读的,即是否有人发消息过来
select_n = select(FD_SETSIZE, &rset, NULL, NULL, NULL);
switch (select_n)
{
case 0:
continue;
case -1:
perror("select error!");
exit(-1);
default:
for (index = 0, in = 0; index < maxi && in < select_n; index++)
{
if (FD_ISSET(client[index], &rset))
{
in++;
recv_and_send_to_all_client(index);//调用函数接收客户端的消息
}
}
}
}
}
}
/*该线程等待客户端连接,并将新的套接字存入全局数组static int client[MAXFD]中*/
void* wait_client_connect(void* pthidcheck)
{
struct sockaddr_in client_addr;
int sin_size, portnumber;
int new_fd = 0;
char clientname[NAMESIZE] = { 0 };
char new_client_inform[NEWCLIENTINFORMSIZE] = { 0 };
static int index = 0;
time_t tt;
struct tm* pTm = NULL;
while (1)
{
/* 服务器阻塞,直到客户程序建立连接 */
if (maxi >= MAXFD)
{
printf("The number of people online has been reached\n");
continue;
}
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr*)(&client_addr), &sin_size)) == -1)
{
fprintf(stderr, "Accept error:%s\n\a", strerror(errno));
exit(1);
}
client[maxi] = new_fd;
if (-1 == read(client[maxi], clientname, sizeof(clientname)))
{
fprintf(stderr, "Read client name error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
maxi++;
tt = time(NULL);
pTm = localtime(&tt);
//'N'表示向所有在线用户通知新用户上线了
sprintf(new_client_inform, "N%d-%d-%d %0d:%0d:%0d",
(1900 + pTm->tm_year), (1 + pTm->tm_mon), (pTm->tm_mday),
(pTm->tm_hour), (pTm->tm_min), (pTm->tm_sec));
sprintf(new_client_inform + TIMESIZE, " A new user %s enters the chat room", clientname);
int outindex = 0;
while (outindex < maxi)
{ //向聊天室中所有的客户通知新用户上线
if (write(client[outindex], new_client_inform, sizeof(new_client_inform)) == -1)
{
fprintf(stderr, "Write Error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
outindex++;
}
/*创建线程收客户端的消息并转发给所有其它在线客户端,同时保存聊天记录*/
if (maxi == 1)
{
if (-1 == pthread_create((pthread_t*)pthidcheck, NULL, check_client_send, NULL))
{
fprintf(stderr, "Creat pthread Error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
fp = fopen("server_msgrecord.txt", "ab");
if (fp == NULL)
{
fprintf(stderr, "Fopen Error:%s\a\n", strerror(errno));
close(sockfd);
exit(1);
}
}
else if (maxi > 1)
{
pthread_cancel(*(pthread_t*)pthidcheck);
if (-1 == pthread_create((pthread_t*)pthidcheck, NULL, check_client_send, NULL))
{
fprintf(stderr, "Creat pthread Error:%s\n", strerror(errno));
fclose(fp);
close(sockfd);
exit(1);
}
}
printf("%s\n", new_client_inform + 1);
printf("%s\n", new_client_inform + TIMESIZE);
fprintf(fp, "%s\n", new_client_inform + 1);
fprintf(fp, "%s\n", new_client_inform + TIMESIZE);
}
}
void* quit(void* qu)
{
char buff[10] = { 0 };
while (1)
{
fgets(buff, sizeof(buff), stdin);
if (strcmp(buff, "quit\n") == 0)
{
break;
}
}
close(sockfd);
fclose(fp);
exit(0);
}
int main(int argc, char* argv[])
{
pthread_t thidaccept, thidcheck = -1,qid;
tcp_init(argc, argv);
printf("Welcome to this chat room\n");
/*创建线程等待接受客户端连接请求*/
if (-1 == pthread_create(&thidaccept, NULL, wait_client_connect, (void*)&thidcheck))
{
fprintf(stderr, "Creat pthread Error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
pthread_create(&qid, NULL, quit, (void*)&thidcheck);//退出线程 方便下次进入
pthread_join(thidaccept, NULL);
if (thidcheck != -1)
{
pthread_join(thidcheck, NULL);
}
close(sockfd);
fclose(fp);
exit(0);
}
客户端代码:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#define PORT 6000 //监听端口
#define NAMESIZE 20 //客户端姓名占用的字节数
#define TIMESIZE 30 //收到消息时的时间占用的字节数
#define SENDBUFSIZE 1024 //消息的数组长度
#define FILENAMESIZE 40 //聊天记录文件名的长度
#define NEWCLIENTINFORMSIZE 70 //通知新用户上线的消息数组长度
FILE* fp; //文件指针,用于指向保存聊天记录的文件
static int sockfd; //绑定服务器端的ip地址和端口的套接字
char clientname[NAMESIZE] = { 0 }; //保存自己的昵称
/*连接服务器*/
void connect_server()
{
struct sockaddr_in server_addr;
struct hostent* host;
char strhost[16];
printf("Please enter the server ip address\n");
scanf("%s", strhost);
//strcpy(strhost, "192.168.0.101");
if ((host = gethostbyname(strhost)) == NULL)
{
fprintf(stderr, "Gethostname error\n");
exit(1);
}
/* 客户程序开始建立 sockfd 描述符 */
printf("The socket is being created...\n");
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Socket Error:%s\a\n", strerror(errno));
exit(1);
}
/* 客户程序填充服务端的资料 */
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr = *((struct in_addr*)host->h_addr);
printf("The socket has been created successfully and is connecting to the server...\n");
/* 客户程序发起连接请求 */
if (connect(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) == -1)
{
fprintf(stderr, "Connect Error:%s\a\n", strerror(errno));
close(sockfd);
exit(1);
}
/* 连接成功了 */
printf("Successfully connected to server \n Welcome to chat room\n");
printf("Please enter your user name\n>");
fflush(stdout);//清空文件缓冲区(或标准输入输出缓冲区)
scanf("%s", clientname);
write(sockfd, clientname, sizeof(clientname));
char recvbuf[SENDBUFSIZE] = { 0 };
if (-1 == read(sockfd, recvbuf, sizeof(recvbuf)))
{
fprintf(stderr, "Read my inform Error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
if (recvbuf[0] == 'N')
{
char filename[FILENAMESIZE] = { 0 };
strcpy(filename, clientname);
strcat(filename, "_msgrecord.txt");
fp = fopen(filename, "ab");
if (fp == NULL)
{
fprintf(stderr, "Fopen Error:%s\a\n", strerror(errno));
close(sockfd);
exit(1);
}
printf("%s\n", recvbuf + 1);
printf("%s\n", recvbuf + TIMESIZE);
fprintf(fp, "%s\n", recvbuf + 1);
fprintf(fp, "%s\n", recvbuf + TIMESIZE);
printf("Start chatting(\"QUIT\"disconnect)\n");
}
}
/*接收服务器消息线程入口函数*/
void* recvfromserver(void* arg)
{
time_t tt;
struct tm* pTm;
char recvbuf[SENDBUFSIZE];
int nbytes = 0;
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
if (0 < read(sockfd, recvbuf, sizeof(recvbuf)))
{
if (recvbuf[0] == 'M')
{ //如果消息是'M'表示消息收到服务器的正常聊天消息
fprintf(fp, "\n%s", recvbuf + 1);
fprintf(fp, " %s", recvbuf + NAMESIZE);
fprintf(fp, " %s", recvbuf + NAMESIZE + TIMESIZE);
fflush(fp);
if (strcmp(recvbuf + 1, clientname) == 0)
{
continue;
}
printf("\n%s", recvbuf + 1);
printf(" %s", recvbuf + NAMESIZE);
printf(" %s", recvbuf + NAMESIZE + TIMESIZE);
}
else if (recvbuf[0] == 'N')
{ //如果是'N'表示收到服务器的新用户通知上线消息
printf("%s\n", recvbuf + 1);
printf("%s\n", recvbuf + TIMESIZE);
fprintf(fp, "%s\n", recvbuf + 1);
fprintf(fp, "%s\n", recvbuf + TIMESIZE);
}
else
{
printf("An unrecognized message was received\n");
}
/*显示时间*/
tt = time(NULL);
pTm = localtime(&tt);
printf("\n%s %d-%d-%d %0d:%0d:%0d\n>", clientname,
(1900 + pTm->tm_year), (1 + pTm->tm_mon), (pTm->tm_mday),
(pTm->tm_hour), (pTm->tm_min), (pTm->tm_sec));
fflush(stdout);
}
}
}
/*向服务器发送消息线程入口函数*/
void* sendtoserver(void* arg)
{
time_t tt;
struct tm* pTm;
char sendbuf[SENDBUFSIZE];
//清除输入缓冲区中的换行符
fgets(sendbuf + NAMESIZE + TIMESIZE, sizeof(sendbuf) - NAMESIZE + TIMESIZE, stdin);
while (1)
{
memset(sendbuf, 0, sizeof(sendbuf));
tt = time(NULL);
pTm = localtime(&tt);
printf("%s %d-%d-%d %0d:%0d:%0d\n>", clientname,
(1900 + pTm->tm_year), (1 + pTm->tm_mon), (pTm->tm_mday),
(pTm->tm_hour), (pTm->tm_min), (pTm->tm_sec));
fflush(stdin);
fflush(stdout);
fgets(sendbuf + NAMESIZE + TIMESIZE, sizeof(sendbuf) - NAMESIZE + TIMESIZE, stdin);
sendbuf[49 + strlen(sendbuf + NAMESIZE + TIMESIZE)] = '\0';
strcpy(sendbuf + 1, clientname);//在消息前面加上自己的呢称
if ((write(sockfd, sendbuf, sizeof(sendbuf))) == -1)
{
fprintf(stderr, "Write Error:%s\n", strerror(errno));
close(sockfd);
fclose(fp);
exit(1);
}
/*如果输入"quit"或"Quit"或"QUIT"都会退出*/
if (strcmp(sendbuf + NAMESIZE + TIMESIZE, "QUIT") == 0 ||
strcmp(sendbuf + NAMESIZE + TIMESIZE, "Quit") == 0 ||
strcmp(sendbuf + NAMESIZE + TIMESIZE, "quit") == 0)
{
close(sockfd);
fclose(fp);
printf("You have quit the chat room\n");
exit(1);
}
}
}
int main(int argc, char* argv[])
{
pthread_t thidrcv, thidsnd;
connect_server(); //连接服务器
/*创建发送和接收消息的线程*/
if (-1 == pthread_create(&thidrcv, NULL, recvfromserver, NULL))
{
fprintf(stderr, "Creat pthread Error:%s\n", strerror(errno));
close(sockfd);
exit(1);
}
if (-1 == pthread_create(&thidsnd, NULL, sendtoserver, NULL))
{
fprintf(stderr, "Creat pthread Error:%s\n", strerror(errno));
close(sockfd);
fclose(fp);
exit(1);
}
pthread_join(thidrcv, NULL);
pthread_join(thidsnd, NULL);
close(sockfd);
fclose(fp);
exit(0);
}