Linux C多人网络聊天室

经过好几天的日夜奋斗,总算把这个聊天室给做出来了,虽然说不上多好,但也是这几天从早到晚劳动的成功,所以就写这篇博文来记录一下啦。别的不敢说,确保能用就是了,完整代码在最后哦~
当然啦,如果有幸被转发,还请注明来处哈~

一、功能

这个Linux下C版本的多人网络聊天室具备以下几个基本功能(或者说需求):
(一)C/S模式,IPv4的TCP通信;
(二)客户端登录需要账号密码,没有账号需要注册;
(三)服务器每接收到一个客户端的连接,就产生一个线程为其服务;
(四)进入聊天室后,客户端可以选择群聊(发给所有人),也可以选择私聊(发给指定账号的人);
(五)服务器可以限定同时登录聊天室的账号的最大数量;
(六)用sqlite3创建保存用户账号信息的数据库;
(七)客户端(服务器端要的话应该也可以)自动获取本地主机未被占用的端口号以连接服务器。

由于时间关系,原本设定的用户登录或退出时通知所有人和保存聊天记录等功能,笔者就没能添加进去。若有兴趣,读者可参考以下想法进行添加:“用户登录或退出时通知所有人”这点可以在用户确认登录后但尚未进入聊天室时通过send/recv实现,而“保存聊天记录”这点就像保存用户账号信息一样保存到数据库(当然更简单的方法是保存到普通文件中)。另外,也有一大点笔者心有余而力不足,有大佬路过时还请指教指教,就是多线程之间交互的问题,例如主线程如何等待所有子线程正常退出等,这一块我不熟悉~

二、主要功能实现分析

(一)网络通信基本步骤
简单来讲就是:客户端——创建套接字,配置网络结构体,绑定,请求连接,正常通信;服务器——创建套接字,配置网络结构体,绑定,监听,接受连接,正常通信。至于网络通信基本原理详细点的介绍可以看我这篇博文介绍——网络通信基本原理。
在这里要注意以下两点:
一个是配置网络结构体,这点其实可以在man手册看到相关(结构体)说明,具体来说就是设定端口号、IP协议(IPv4/IPv6)以及通信对象的IP地址,配置网络结构体是为了后续bind服务的。

 //配置网络结构体
  struct sockaddr_in myaddr; 
  myaddr.sin_port = htons(50000);	//应用层端口号
  myaddr.sin_family = AF_INET;		//IPv4协议
  myaddr.sin_addr.s_addr = inet_addr("0.0.0.0");//通信对象的IP地址,0.0.0.0表示所有IP地址,也可以指定IP

另一个是所谓的“自动获取本地主机未被占用的端口号”。由于笔者只是在自己一台电脑上开多个虚拟终端来运行client,连接的服务器在本地主机(127.0.0.1),而我又不想手动地一个客户端给他传递一个端口号,所以就想到了这个方法。具体来讲,就是从50001开始随便一个一个去尝试bind,绑定失败就下一个,绑定成功的话那就它了。至于为什么是50001开始呢,一个依据是《计算机网络》中说到客户端使用的端口号为49152~65535,然后我就在这个范围内选个中意的范围了。

portnum = 50001;
  while(1){
    if(portnum > 65535){
      printf("bind error because of no port number available!\n");
      return -1;
    }
    (.......配置网络结构体)
    ret = bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
    if(ret == -1) {
      continue;
    }
    else{
      printf("bind successfully!\n");
      break;
    }
  }

(二)用户账号信息如何验证、存储
讲到这点,我们得先看看数据库方面的知识了,这里我用的是SQLite,其操作简单讲就是增、删、查、改这些,具体地C接口在这个程序中我使用的有sqlite3_open_v2()、sqlite3_exec()、sqlite3_get_table()。其实说到数据库,我也是在做这个小项目过程中现学现卖的,所以就不多班门弄斧了,不过一些基本操作咱可以看这个网站。在这里我就介绍下我怎么存储怎么验证。
(1)存储
说到存储,在我这个小项目里那就只能是在注册过程才会用到了,而注册是在客户端进行,所以要先获取ID信息(昵称和密码),然后发送给服务器服务器读取用户账号信息表格并分配一个尚未使用的ID,然后将账号信息(ID,name,passwd)插入到数据库,而插入成功了也就是说注册成功了,那就要返回一个注册成功标志给客户端

//这个代码段是用来计算用户账号信息表中有多少条记录的,将这个数量加上起始的100的结果作为账号分配,就能使得账号不重复了。
int j = 100;
	char *sql1 = "select * from hwy_id_sheet;";
	sqlite3_get_table(db,sql1,&azResult,&nrow,&ncolumn,&errmsg);
	j = j+ nrow;
	memset(ack_ID_info,0,4);
	sprintf(ack_ID_info,"%d",j);//---itoa

(2)验证登录
验证起始也跟前面“存储”差不多,就是:客户端获取登录信息(ID,passwd)–发送给服务器–服务器在用户账号信息表中查找是否有相关记录,有则代表登录成功,没有就代表登录失败–返回登录状态–若登录成功则也返回(name)。

sprintf(sql,"select * from hwy_id_sheet where id = '%s' and passwd = '%s';",
			client_ID_info.id,client_ID_info.passwd);
	sqlite3_get_table(db,sql,&azResult,&nrow,&ncolumn,&errmsg);

(三)群聊OR私聊
这好办,程序中,当写入的聊天信息中“目标ID”为“999”时发给所有人,也就是所谓的群聊;当写入的聊天信息中“目标ID”为其他ID如“101”时,则发给对应用户。

void SendMsgToAll(char *msg)
{
  int i;
  for(i=0;i
void SendMsgToSb(int destfd,char *msg)
{
  int i;
  for(i=0;i

(四)服务器端与客户端对应的fd和ID如何绑定?
因为服务器端accept客户端后会产生client_sockfd,之后服务器就用这个client_sockfd来与客户端通信,然而我们私聊时是无法得知服务器给某个客户端分配了哪一个client_sockfd的,只能通过ID来指定通信用户,所以就产生了这个问题了。至于怎么绑定,看下面结构体就知道了~

//id--fd结构体类型
typedef struct
{
	int client_fd;
	char client_id[4];
}client_id_to_fd;

(1)服务器每接收到客户端连接时就会产生一个对应的fd,这事写进client_fd中;
(2)服务器验证客户端登录账号成功时会获取一个正确的ID,这时再写进去。
由此也就实现了这一绑定了,之后怎么通信,看上面步骤(三)。

三、完整代码

(1)头文件–hwy_network_chat.h

#ifndef __HWY_NETWORK_CHAT_H__
#define __HWY_NETWORK_CHAT_H__

//聊天室中能注册登陆的最大人员数量
#define  PEOPLE_NUM_MAX   10
//聊天信息(发送结构体)长度
#define  CHAT_STRUCT_SIZE (POSITION_CONTENT+128)
//客户端最大连接数量
#define  CLIENT_MAX       3

#define  POSITION_SELFID  0
#define  POSITION_NAME    (4+POSITION_SELFID)
#define  POSITION_DESTID  (4+POSITION_NAME)
#define  POSITION_TIME    (4+POSITION_DESTID)
#define  POSITION_CONTENT (26+POSITION_TIME)

//聊天室中人员身份信息
typedef struct 
{
  char id[4];	 //登陆帐号
  char name[4];	 //昵称
  char passwd[8];//登陆密码
}hwy_people_t;

//聊天消息之发送的格式
typedef struct
{
  char self_id[4];	//自身ID
  char name[4];		//自身昵称
  char dest_id[4];	//目标ID
  char time[26];	//发送时间
  char content[128];	//发送内容
}hwy_send_msg_t;

//聊天消息之接收的格式
typedef struct
{
  char who[4];		//发送者
  char time[26];	//发送时间
  char content[128];	//发送内容
}hwy_recv_msg_t;



#endif

(2)client.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "hwy_network_chat.h"
#include 

//登录者帐号信息
static hwy_people_t client_id;

/*
 * 功能:建立网络通信的初始化工作,包括创建套接字和绑定可用端口号
 * 输入参数:无
 * 输出参数:成功返回用于网络通信的套接字,失败返回-1
 */
int sock_init(void)
{
  int ret;
  int portnum;//端口号
  //创建套接字--设置通信为IPv4的TCP通信
  int sockfd = socket(AF_INET,SOCK_STREAM,0);
  if(sockfd == -1)
  {
    perror("socket failed");
    return -1;
  }
  
  //配置网络结构体
  /*从50001开始,查找主机中尚未被占用的端口号,然后绑定
   */
  struct sockaddr_in myaddr;
  portnum = 50001;
  while(1){
    if(portnum > 65535){
      printf("bind error because of no port number available!\n");
      return -1;
    }
    myaddr.sin_port = htons(portnum++);       //应用层端口号
    myaddr.sin_family = AF_INET;          //IPv4协议
    myaddr.sin_addr.s_addr = inet_addr("0.0.0.0");//通信对象的IP地址,0.0.0.0表示所有IP地址
    //绑定套接字和结构体----主机自检端口号是否重复,IP是否准确
    ret = bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
    if(ret == -1)
    {
      continue;
    }
    else{
      printf("bind successfully!\n");
      break;
    }
  }
  return sockfd;
}

/*
 *功能:连接服务器
 *输入参数:套接字
 *输出参数:连接成功与否的标志,成功返回1,失败返回0
 * */
int sock_client(int sockfd)
{
  //连接服务器
  struct sockaddr_in srcaddr;
  srcaddr.sin_port = htons(50000);//服务器的应用层端口号
  srcaddr.sin_family = AF_INET;//服务器的IPv4协议
  srcaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//服务器的IP地址-本地主机

  int ret = connect(sockfd,(struct sockaddr*)&srcaddr,sizeof(srcaddr));
  if(ret == -1)
  {
    perror("connect failed");
    return -1;
  }
  printf("connect OK\n");
  return 0; 
}



/*
 *功能:验证登录或注册帐号信息
 *输入:客户端套接字,登录/注册选择
 *输出:登录成功1,注册成功2,任意失败-1
 * */
int check_id(int sockfd,int choice)
{
  char ID_info[17];//存储帐号信息以及登录/注册选择
  int i;
  char status[16];//存储登录/注册状态
  int ret;

  printf("check ID information!\n");
  memset(ID_info,0,17);
  //登录,携带ID 和密码
  if(1 == choice){
    ID_info[0]='1';
    memcpy(ID_info+1,client_id.id,4);
    memcpy(ID_info+9,client_id.passwd,8);
  }

  //注册,携带昵称和密码
  else {
    ID_info[0]='2';
    memcpy(ID_info+5,client_id.name,4);
    memcpy(ID_info+9,client_id.passwd,8);
  }

  //发送帐号信息给服务器端进行验证
  for(i=0;i<16;i++){
  	if(ID_info[i] == '\0'){
		ID_info[i] = '/';
	}
  }
  ID_info[i] = '\0';
  ret = send(sockfd, ID_info, strlen(ID_info), 0);
  if(-1 == ret){
    perror("send id_info error!");
	return -1;
  }

  //接收帐号验证信息
  memset(status,0,16);
  ret = recv(sockfd,status,16,0);
  if(-1 == ret){
    perror("recv id_info error!");
	return -1;
  }
  
  if(memcmp(status,"successfully!",13)==0){
    //登录成功
    //printf("login successfully!\n");
	send(sockfd,"ok",3,0);
	ret = recv(sockfd,client_id.name,4,0);
	if(-1 == ret){
		perror("recv ack_id_info error");
		return -1;
	}
	return 1;
  }
  else if(memcmp(status,"sign up",7)==0){
    //注册成功
    //printf("sign up successfully!\n");
	send(sockfd,"ok",3,0);
	ret = recv(sockfd,client_id.id,4,0);
	if(-1 == ret){
		perror("recv ack_id_info error");
		return -1;
	}
	return 2;
  }
  
  else 
  {
    printf("login or sign up error!\n");
	return -1;
  }
}


/*
 *功能:登录界面
 *输入:客户端套接字
 *输出:成功0,失败-1
 *注意:登录失败可以重新登录,其他失败会退出
 */
int hwy_login(int sockfd )
{
  int choice;//1--代表登录,2代表注册
  int login_status;//登录状态,-1代表失败,1代表成功
  int signup_status;//注册状态,-1代表失败,2代表成功

  while(1){
    printf("1------------login\n2------------sign up\n");
    scanf("%d",&choice);
    if(1 == choice){
      //登录,输入ID和密码
      printf("ID:");
      scanf("%s",client_id.id);  
      printf("passwd:");
      scanf("%s",client_id.passwd);

      login_status=check_id(sockfd,choice);
	  if(1 == login_status){
	  	//登录成功,进入聊天室
		printf("欢迎登录聊天室~%s\n",client_id.name);
		break;
	  }
	  else 
      	continue;
    }
    else if(2 == choice){
	  //注册,输入昵称和密码
      printf("name:");
	  scanf("%s",client_id.name);
	  printf("passwd:");
	  scanf("%s",client_id.passwd);
	  signup_status = check_id(sockfd,choice);
	  if(2 == signup_status){
	  	//注册成功,返回登录界面
		printf("注册成功\n");
		printf("你的帐号为:%s\n请重新登录\n",client_id.id);
		continue;
	  }
	  else {
	  	//注册失败
		return -1;
	  }
    }
    else {
      printf("错误!请输入正确数值!\n");
      continue;
    }
  }
  return 0;
}


/*
 *功能:获取聊天具体内容
 *输入:保存聊天内容的指针
 *输出:无
 * */
//获取聊天具体内容
void get_send_content(char get_send_buffer[CHAT_STRUCT_SIZE])
{
  int i;
  char dest[4];//目标帐号
  char time_buf[26];//时间
  time_t t;
  time(&t);
  memset(get_send_buffer,0,CHAT_STRUCT_SIZE);
  //发送者
  memcpy(get_send_buffer+POSITION_SELFID,client_id.id,4);
  memcpy(get_send_buffer+POSITION_NAME,client_id.name,4);
  //接收者
  //printf("你要发给谁?\n");
  scanf("%s",get_send_buffer+POSITION_DESTID);
  //发送内容
  //printf("你要发什么?\n");
  scanf("%s",get_send_buffer+POSITION_CONTENT);
  //发送时间
  memcpy(get_send_buffer+POSITION_TIME,ctime_r(&t,time_buf),26);  
  for(i=0;i

(3)server.c

#include      
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "hwy_network_chat.h"
#include "sqlite3.h"
#include 


//id--fd结构体类型
typedef struct
{
	int client_fd;
	char client_id[4];
}client_id_to_fd;

//将用户帐号和占用的文件描述符一一对应起来,
//方便后续一对一通信
client_id_to_fd id_to_fd[CLIENT_MAX];

//数据库的连接指针
static sqlite3 *db = NULL;

/*
 *功能:建立网络通信的初始化工作,包括创建套接字和绑定可用端口号
 *输入:无
 *输出:成功返回套接字,失败返回-1
 * */
int sock_init(void )
{
  int ret;
  //创建套接字--设置通信为IPv4的TCP通信
  int sockfd = socket(AF_INET,SOCK_STREAM,0);
  if(sockfd == -1)
  {
    perror("socket failed");
    return -1;
  }

  //配置网络结构体
  struct sockaddr_in myaddr; 
  myaddr.sin_port = htons(50000);	//应用层端口号
  myaddr.sin_family = AF_INET;		//IPv4协议
  myaddr.sin_addr.s_addr = inet_addr("0.0.0.0");//通信对象的IP地址,0.0.0.0表示所有IP地址

  //绑定套接字和结构体----主机自检端口号是否重复,IP是否准确
  ret = bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
  if(ret == -1)
  {
    perror("bind failed");
    return -1;
  }
  
  return sockfd;
}

/*
 *功能:把服务器接收的信息发给所有人
 *输入:聊天信息具体内容
 *输出:无
 * */
void SendMsgToAll(char *msg)
{
  int i;
  for(i=0;i0){
      printf("接收到的内容为:%s\n",recv_buffer);

      if(memcmp(recv_buffer+POSITION_DESTID,"999",3)== 0)
        SendMsgToAll(recv_buffer);
      else{
		for(i = 0;i< CLIENT_MAX;i++){
			if(memcmp(id_to_fd[i].client_id,recv_buffer+POSITION_DESTID,\
				3)== 0){
        		SendMsgToSb(id_to_fd[i].client_fd,recv_buffer);
				break;
			}
		}
	  
	  }
    }
  
  }  
}

void service(int sock_fd)
{
  printf("服务器启动...\n");
  listen(sock_fd,CLIENT_MAX);
  while(1){
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int client_sock = accept(sock_fd,(struct sockaddr*)&clientaddr,&len);
    if(client_sock== -1)
    {
      printf("accept failed...\n");
      continue;
    }
    printf("accept OK!\n");
    int i;
    for(i=0;i

(3)数据库相关代码
sqlite3.c sqlite3.h这个可以下载。

(4)编译方法
gcc client.c -lpthread -o client
gcc server.c sqlite.c -ldl -lpthread -o server
(路过的还请赐教下这个怎么写Makefile??~)

你可能感兴趣的:(网络编程,多人网络聊天室,Linux网络聊天室,聊天室,Linux,C,多人网络聊天室)