序
项目简介:采用I/O复用技术select实现socket通信,采用多线程负责每个客户操作处理,完成Linux下的多客户聊天室!
OS:Ubuntu 15.04
IDE:vim gcc make
DB:Sqlite 3
Time:2015-12-09 ~ 2012-12-21
项目功能架构:
- 采用client/server结构;
- 给出客户操作主界面(注册、登录、帮助和退出)、登录后主界面(查看在线列表、私聊、群聊、查看聊天记录、退出);
- 多客户可同时连接服务器进行自己操作;
部分操作如下图所示:
Make命令:
1. 多用户注册:
(1)服务器监听终端
(2)用户注册终端
- 多用户登录、聊天:
(1)用户yy登录
(2)用户rr登录
(3)服务器监听终端
程序结构
公共数据库
chatRome.db
服务器端
- server.c:服务器端主程序代码文件;
- config.h:服务器端配置文件(包含需要的头文件、常量、数据结构及函数声明);
- config.c:服务器端公共函数的实现文件;
- list.c:链表实现文件,用于维护在线用户链表的添加、更新、删除操作;
- register.c:服务器端实现用户注册;
- login.c:服务器端实现用户登录;
- chat.c:服务器端实现用户的聊天互动操作;
- Makefile:服务器端make文件,控制台执行make命令可直接生成可执行文件server
客户端
1. client.c:客户端主程序代码文件;
2. config.h:客户端配置文件(包含需要的头文件、常量、数据结构及函数声明);
3. config.c:客户端公共函数的实现文件;
4. register.c:客户端实现用户注册;
5. login.c:客户端实现用户登录;
6. chat.c:客户端实现用户的聊天互动操作;
7. Makefile:客户端make文件,控制台执行make命令可直接生成可执行文件client;
8. interface.c:客户端界面文件;
源码
服务器端
server.c
/*******************************************************************************
* 服务器端程序代码server.c
* 2015-12-09 yrr实现
*
********************************************************************************/
#include "config.h"
/*定义全局变量 -- 在线用户链表*/
ListNode *userList = NULL;
/*********************************************
函数名:main
功能:聊天室服务器main函数入口
参数:无
返回值:正常退出返回 0 否则返回 1
**********************************************/
int main(void)
{
/*声明服务器监听描述符和客户链接描述符*/
int i , n , ret , maxi , maxfd , listenfd , connfd , sockfd;
socklen_t clilen;
pthread_t pid;
/*套接字选项*/
int opt = 1;
/*声明服务器地址和客户地址结构*/
struct sockaddr_in servaddr , cliaddr;
/*声明描述符集*/
fd_set rset , allset;
//nready为当前可用的描述符数量
int nready , client_sockfd[FD_SETSIZE];
/*声明消息变量*/
Message message;
/*声明消息缓冲区*/
char buf[MAX_LINE];
/*UserInfo*/
User user;
/*(1) 创建套接字*/
if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
perror("socket error.\n");
exit(1);
}//if
/*(2) 初始化地址结构*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
/*(3) 绑定套接字和端口*/
setsockopt(listenfd , SOL_SOCKET , SO_REUSEADDR , &opt , sizeof(opt));
if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
perror("bind error.\n");
exit(1);
}//if
/*(4) 监听*/
if(listen(listenfd , LISTENEQ) < 0)
{
perror("listen error.\n");
exit(1);
}//if
/*(5) 首先初始化客户端描述符集*/
maxfd = listenfd;
maxi = -1;
for(i=0; i<FD_SETSIZE; ++i)
{
client_sockfd[i] = -1;
}//for
/*清空allset描述符集*/
FD_ZERO(&allset);
/*将监听描述符加到allset中*/
FD_SET(listenfd , &allset);
/*(6) 接收客户链接*/
while(1)
{
rset = allset;
/*得到当前可读的文件描述符数*/
nready = select(maxfd+1 , &rset , NULL , NULL , 0);
/*测试listenfd是否在rset描述符集中*/
if(FD_ISSET(listenfd , &rset))
{
/*接收客户端的请求*/
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
{
perror("accept error.\n");
exit(1);
}//if
printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr));
/*查找空闲位置,设置客户链接描述符*/
for(i=0; i<FD_SETSIZE; ++i)
{
if(client_sockfd[i] < 0)
{
client_sockfd[i] = connfd; /*将处理该客户端的链接套接字设置在该位置*/
break;
}//if
}//for
if(i == FD_SETSIZE)
{
perror("too many connection.\n");
exit(1);
}//if
/* 将来自客户的连接connfd加入描述符集 */
FD_SET(connfd , &allset);
/*新的连接描述符 -- for select*/
if(connfd > maxfd)
maxfd = connfd;
/*max index in client_sockfd[]*/
if(i > maxi)
maxi = i;
/*no more readable descriptors*/
if(--nready <= 0)
continue;
}//if
/*接下来逐个处理连接描述符*/
for(i=0 ; i<=maxi ; ++i)
{
if((sockfd = client_sockfd[i]) < 0)
continue;
if(FD_ISSET(sockfd , &rset))
{
/*如果当前没有可以读的套接字,退出循环*/
if(--nready < 0)
break;
pthread_create(&pid , NULL , (void *)handleRequest , (void *)&sockfd);
}//if
/*清除处理完的链接描述符*/
FD_CLR(sockfd , &allset);
client_sockfd[i] = -1;
}//for
}//while
close(listenfd);
return 0;
}
/*处理客户请求的线程*/
void* handleRequest(int *fd)
{
int sockfd , ret , n;
/*声明消息变量*/
Message message;
/*声明消息缓冲区*/
char buf[MAX_LINE];
sockfd = *fd;
memset(buf , 0 , MAX_LINE);
memset(&message , 0 , sizeof(message));
//接收用户发送的消息
n = recv(sockfd , buf , sizeof(buf)+1 , 0);
if(n <= 0)
{
//关闭当前描述符,并清空描述符数组
fflush(stdout);
close(sockfd);
*fd = -1;
printf("来自%s的退出请求!\n", inet_ntoa(message.sendAddr.sin_addr));
return NULL;
}//if
else{
memcpy(&message , buf , sizeof(buf));
/*为每个客户操作链接创建一个线程*/
switch(message.msgType)
{
case REGISTER:
{
printf("来自%s的注册请求!\n", inet_ntoa(message.sendAddr.sin_addr));
ret = registerUser(&message , sockfd);
memset(&message , 0 , sizeof(message));
message.msgType = RESULT;
message.msgRet = ret;
strcpy(message.content , stateMsg(ret));
memset(buf , 0 , MAX_LINE);
memcpy(buf , &message , sizeof(message));
/*发送操作结果消息*/
send(sockfd , buf , sizeof(buf) , 0);
printf("注册:%s\n", stateMsg(ret));
close(sockfd);
*fd = -1;
return NULL;
break;
}//case
case LOGIN:
{
printf("来自%s的登陆请求!\n", inet_ntoa(message.sendAddr.sin_addr));
ret = loginUser(&message , sockfd);
memset(&message , 0 , sizeof(message));
message.msgType = RESULT;
message.msgRet = ret;
strcpy(message.content , stateMsg(ret));
memset(buf , 0 , MAX_LINE);
memcpy(buf , &message , sizeof(message));
/*发送操作结果消息*/
send(sockfd , buf , sizeof(buf) , 0);
printf("登录:%s\n", stateMsg(ret));
/*进入服务器处理聊天界面*/
enterChat(&sockfd);
break;
}//case
default:
printf("unknown operation.\n");
break;
}//switch
}//else
close(sockfd);
*fd = -1;
return NULL;
}
config.h
/*******************************************************************************
* 基本配置文件 -- 包含所需头文件
* 用户信息结构体定义
* 在线用户链表定义
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <memory.h> /*使用memcpy所需的头文件*/
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#include <pthread.h>
#include <sqlite3.h>
/*FD_SETSIZE定义描述符集的大小,定义在sys/types.h中*/
#ifndef FD_SETSIZE
#define FD_SETSIZE 256
#endif
#define MAX_LINE 8192
#define PORT 8888
#define LISTENEQ 6000
/*预定义数据库名称*/
#define DB_NAME "/home/yangrui/projects/Socket/ChatRome_select/chatRome.db"
/*标志*/
enum Flag{
YES, /*代表被禁言*/
NO /*代表没有被禁言*/
};
/*定义服务器--客户端 消息传送类型*/
enum MessageType{
REGISTER = 1, /*注册请求*/
LOGIN, /*登陆请求*/
HELP, /*帮助请求*/
EXIT, /*退出请求*/
VIEW_USER_LIST, /*查看在线列表*/
GROUP_CHAT, /*群聊请求*/
PERSONAL_CHAT, /*私聊请求*/
VIEW_RECORDS, /*查看聊天记录请求*/
RESULT, /*结果消息类型*/
UNKONWN /*未知请求类型*/
};
/*定义操作结果 */
enum StateRet{
EXCEED, //已达服务器链接上限
SUCCESS, //成功
FAILED, //失败
DUPLICATEID, //重复的用户名
INVALID, //不合法的用户名
ID_NOT_EXIST, //账号不存在
WRONGPWD, //密码错误
ALREADY_ONLINE, //已经在线
ID_NOT_ONLINE, //账号不在线
ALL_NOT_ONLINE, //无人在线
MESSAGE_SELF //消息对象不能选择自己
};
/*定义服务器 -- 客户端 消息传送结构体*/
typedef struct _Message{
char content[2048]; /*针对聊天类型的消息,填充该字段*/
int msgType; /*消息类型 即为MessageType中的值*/
int msgRet; /*针对操作结果类型的消息,填充该字段*/
struct sockaddr_in sendAddr; /*发送者IP*/
struct sockaddr_in recvAddr;
char sendName[20]; /*发送者名称*/
char recvName[20]; /*接收者名称*/
char msgTime[20]; /*消息发送时间*/
}Message;
//用户信息结构体
typedef struct _User{
char userName[20]; //用户名
char password[20];
struct sockaddr_in userAddr; //用户IP地址,选择IPV4
int sockfd; //当前用户套接字描述符
int speak; //是否禁言标志
char registerTime[20]; //记录用户注册时间
}User;
/*定义用户链表结构体*/
typedef struct _ListNode{
User user;
struct _ListNode *next;
}ListNode;
/*定义在线用户链表*/
extern ListNode *userList;
/*server.c 客户请求处理函数*/
extern void* handleRequest(int *fd);
/*config.c文件函数声明*/
extern char *stateMsg(int stateRet);
extern void copyUser(User *user1 , User *user2);
/*chat.c文件函数声明*/
extern void enterChat(int *fd);
extern int groupChat(Message *msg , int sockfd);
extern int personalChat(Message *msg , int sockfd);
extern int viewUserList(Message *msg , int sockfd);
extern int viewRecords(Message *msg , int sockfd);
/*list.c文件函数声明*/
extern ListNode* insertNode(ListNode *list , User *user);
extern int isOnLine(ListNode *list , User *user);
extern void deleteNode(ListNode *list , User *user);
extern void displayList(ListNode *list);
/*login.c文件函数声明*/
extern int loginUser(Message *msg , int sockfd);
/*register.c文件函数声明*/
extern int registerUser(Message *msg , int sockfd);
config.c
/************************************