前言
在学习Linux网络编程过程中,由于初涉socket,对网络编程的实现知之甚少,所以本文记录一下我学习socket时跟着一口Linux学习的--------基于Linux socket搭建的聊天室,功能包括注册、登录、公聊、私聊、在线列表。
进程间通信
简称IPC,指两个进程之间的通信。系统中的每一个进程都有自己的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。
Linux内核提供了多种IPC机制,其中,基于套接字(Socket,也就是网络)的进程间机制,允许位于同一主机或使用网络连接起来的不同主机上的应用程序之间交换数据,简称网络通信。
在客户端/服务器端场景中,应用程序使用socket进行通信方式如下: 各个应用程序创建一个socket;服务器将自己的socket绑定到一个众所周知的地址上使得客户端可以定位到他的位置。
不同主机上的应用程序之间的网络通信问题分为三个层次:
如图,在应用层中,应用程序是基于内核的socket接口进行应用编程,实现自己的网络应用程序。除了socket接口以外,应用层还会使用一些更高级的接口,如http、网络控件等,它们都是对socket接口的一种更高级的封装。而内核向应用层提供了socket接口,可以看成socket是一个门口,他把tcp/ip协议隐藏在接口后面。
socket()函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
类似open()函数,创建一个网络通信端点(打开一个网络通信),成功就返回一个网络文件描述符,其可以用来进行读写操作。
bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
将一个IP地址或者端口号与一个套接字绑定。即将一个服务器的套接字绑定到一个客户端应用提前就知道的地址(如127.0.0.1 和 8888)
C/S架构
因为聊天室用到了客户端和服务端,所以也学习一下这个架构。C/S架构就是服务器-客户机,通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。 如图,基于套接字实现客户端和服务器相连的函数调用关系,这个图来源于一口linux大佬,我觉得他把客户端和服务端之间的关系描述得太好了!
pthread线程库
线程库的主要功能包括:
- 线程的创建和销毁:使用
pthread_create()函数可以创建一个新的线程,并指定该线程将要执行的函数和参数,使用pthread_join()函数可以等待指定线程执行完毕,并回收其资源。 - 线程的同步:使用互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等机制可以对多个线程的访问和操作进行同步控制,避免出现资源竞争和数据不一致的问题。
- 线程的调度和优先级控制:使用
pthread_attr_t结构体可以设置线程的栈大小、调度策略、优先级等属性,从而实现自定义的调度和优先级控制。 - 线程的取消和异常处理:使用
pthread_cancel()函数可以取消指定线程的执行,使用pthread_cleanup_push()和pthread_cleanup_pop()函数可以注册和执行清理函数,从而保证线程在取消或异常情况下也能正确地释放资源和撤销操作。
在进行编译时要加上 -lpthread,后面的sqlite3库也是要加上 -lsqlite3,这里给我埋了小坑😂。
多线程并发服务器
分为主线程和子线程。搭建聊天室也正是用了这个模型,这一点一口Linux大佬也讲的很好,把我这个菜鸡讲明白了。
主线程
就是mian函数,它的任务是: 1. socket()创建监听套接字; 2. bind()绑定服务器的端口号和地址; 3. listen()开启监听; 4. accept()等待客户端的连接; 5. 当连接客户端的时候,accept()创建一个新的套接字new_fd并且申请一个新的内存来存放 6. 主线程创建子线程后就把new_fd传给子线程。
子线程
子线程就通过new_fd处理和客户端所有的通信任务。
功能实现
注册和登录
实现注册和登录两个功能,就需要让服务器和客户端在交互数据包的时候按照统一的格式收发信令,这里是c/s结构里面的变量cmd对应的。
- 服务器先启动然后监听是否有客户端的连接;
- 客户端启动,首先连接服务器,显示登录、注册界面;
- 服务器接收到客户端连接之后,就会创建一个子线程用于客户端的通信(这里我认为就是多线程并发服务器模型的体现)
- 选择注册后,输入信息后,就会封装信息到结构体变量msg中,并且发送信令到服务器上。
write(sockfd,&msg,sizeof(msg));
read(sockfd,&msgback,sizeof(msgback));
- 服务器接收到客户端注册信息后,会进到数据库查找,如果没有找到说明是未注册过的,可以通过。
dest_index = db_user_if_reg(msg->name);
if(dest_index == -1)
{ // this user can registe
*index = db_add_user(msg->name,msg->data);
msg_back.state = OP_OK;
printf("user %s regist success!\n",msg->name);
write(sockfd,&msg_back,sizeof(msg_back));
return;
}else{
msg_back.state = NAME_EXIST;
printf("user %s exist!\n",msg->name);
write(sockfd,&msg_back,sizeof(msg_back));
return;
}
- 在登录功能中,也是和注册差不多的流程,首先在数据库中查找是否有这个人,接着查找他是否在线,如果不在线就设置在线这个状态并且同步到数据库里面去。返回登录成功信息给客户端。
公聊和私聊
- 客户端登陆后在菜单选择公聊或者私聊功能
- 如果是私聊要输入聊天的对象和聊天信息,公聊就直接输入聊天信息就行
- 发送聊天信息给服务器,服务器的子线程收到相应数据之后,就进入对应流程
- 查找对应用户然后发送信息
void broadcast(int fd,char *name,struct protocol*msg)
{
int i;
char buf[128]={0};
sprintf(buf,"%s say:%s\n",name ,msg->data);
db_broadcast(fd,buf);
}
void private(int fd,char *name,struct protocol*msg)
{
int dest_index;
char buf[128]={0};
sprintf(buf,"%s say:%s\n",name ,msg->data);
db_private(fd,msg->name,buf);
}
- 在这两种聊天方式中,都是通过客户端创建一个子线程进行接收服务器发送的数据信息
在线用户列表
- 客户端先选择显示在线用户功能
- 然后封装该数据包并且发送给服务器
- 服务器收到数据包后,进入显示在线用户模块,然后检查数据库
- 把状态位不为-1的名称封装到数据包发送给客户端
- 客户端子线程查找该数据包并提取信息,使用
sqlite3_get_table()函数可以直接查询数据库全部内容并返回,这样就不用设置stat位了,该位需要设置开始位和结束位,只有结束位则会提示用户显示完毕。
数据库
这个项目用到了Sqlite3,操作基本和mysql一样,但是值得注意的是用c语言来操作sqlite,这是从一口Linux学到的。
最后再总结一下,这个项目用到了socket、线程(怎么创建线程pthread_create)和多线程并发(这里我感觉我还有点云里雾里)、C\S结构、sqlite的操作