聊天1.0
需求:
创建两个工程,服务器工程和客户端工程,服务器先启动
客户端使用socket技术,连接服务器,connect连接服务器以后,创建两个线程,一个负责读一个负责写,写线程通过控制台输入,发送数据给服务器
服务器启动之后accept函数接收到一个客户端connect连接后,就创建一个线程专门针对连接成功的这个客户端acceptfd进行读操作,读到客户端发送数据并打印
尝试运行多个客户端进程,登录成功以后互相进行一对一单聊业务
框架:
- 服务器需要在客户端连接成功一次,创建一个线程,服务器是被动先接收到数据,才会进行写数据的操作(先read在write,然后read函数又是阻塞的,因此就是要利用read的阻塞)
- 客户端创建两个线程,一个读取线程,一个写数据线程,读写要同时发生,所以客户端必须要是两条独立的线程
在对于结构体(自定义通信协议雏形)时,要额外注意结构体的大小
自定义通信协议通常以:定长包头+不定长包体
包头在目前业务指:业务类型+业务长度(内存大小)
包体在目前业务指:具体的业务数据(比如说账号密码聊天内容…)
1.0版本推荐采用struct结构体复合型数据来
这边需要创建一个头文件,把结构体放入这个.h文件
注意点:Map不要用指针做key
因为map的键值是识别一个指,指针的话是存储一个地址,只要地址不变(没有一直malloc)就会一直都是同一个地址,导致map一直都只能存一个数据
我们在客户端利用pthread_create创建线程时,可以将前面创建并且和服务器绑定的套接字传输给线程处理函数(write版本),然后通过write函数发送账号密码数据给服务器
客户端首次发送账号密码数据时的注意点:
首先我们要使用复合型数据结构(结构体)来存储账号密码和对应的请求(登录\注册),在这边我们采用头+体的结构模式,将类型(登录类型\聊天类型)存储在头结构体(HEAD)中,再将头体结构中的体的数据大小(sizeof(各种复合型数据结构))存储在头结构体的一个属性中,如下图第一个方框所示
其次对于拼接头+体两个结构体要注意需要在第二次memcpy时进行指针偏移的操作
客户端首次发送账号密码数据时的注意点:
Write发送给服务器只算有效数据字节,不用sizeof(buf)的原因是因为服务器那边是死循环读取,如果是sizeof(buf)的话,由于前面已经对buf进行初始化,因此在200的空间中不一定每次都会占用完所有空间,未被占用的都会是0,这样在服务器死循环就会一直读0
如果使用sizeof(buf)会出现这种情况
所以我们的write的第三个参数要按下面的形式来写
到目前为止我们可以实现从客户端向服务器发送账号密码的一个操作,接下来我们要从服务器的视角来接收这个数据
注意点:服务器端读取数据时必须先读头
因为我们write传过来的是一个char类型的数组,如果我们直接读取这个数组会出现一些未知符号,这个时候我们就需要通过memcpy的方式拿出头的数据(这边需要一个空的头结构体接收),这个过程就像是加密解密的过程,在写入传输时我们利用memcpy将结构体加密成一个char数组,在读取时利用memcpy的操作将这个char数组的数据解密出来,我们通过先读取头结构体的操作,利用头结构体中的类型属性来区分接下来要做的业务
注意点:服务器读取客户端发来的体数据中的read函数的第三个参数要用客户端发来的数据
这边插入一个小tip:
因为TPC传输不一定百分百成功
下面是三种传输意外的情况:
注意点:客户端发送一次,服务器必须要读取两次
原因如下:
执行操作:A发送给B信息
流程:A先发给服务器,服务器再发给B,这个时候我们要保证时间的统一性,尽量避免A发了信息,A这边显示已经发送,B过了10秒才出现在屏幕上的这种情况,因此我们直接write两次,分别对发送者和接收者的套接字进行write
对于Linux的文件跳过手动编译步骤直接执行的方法
目前来说存在的问题:
这边的Address是指端口
前面我们说到如何实现客户端请求登录和服务器反馈登录信息的注意点,下面我们来讲述如何进行聊天的主要操作
首先第一步就是想到write函数
这个函数在这边最主要的参数就是套接字
因此我们现在最主要的目标就是获取到要聊天对象的套接字
我们在服务器利用pthread_create这个函数创建线程时,将accept函数的返回值(某个客户端的套接字)传入这个线程处理函数,在这个线程处理函数中,先解出前面传过来的buf数组中的账号,然后利用map容器插入进去,在插入之前我们可以用count函数来判断该用户是否已经登录,如果未登录就将套接字和账号通过map容器的特性绑定起来
If(map.count(u.username))
这步操作之后我们就实现了查找要聊天对象的那个套接字
注意点:
在客户端我们使用一个全局的bool类型的变量来判断是否登录成功,由此来决定 客户端是在登录状态(请输入账号密码)还是在聊天状态(请输入聊天对象)
这个时候要注意,在write的线程处理函数的死循环末尾要sleep等待,这个原因就是因为read的速度会慢于write的速度,所以write要有缓冲时间
这边可以之间使用之前登录的ID
服务器:
在接收到数据之后,必须要给发送方和接收方都发送聊天内容的数据
因此在客户端的读线程处理函数中需要判断是接收方还是发送方
这个时候需要用一个全局变量在登录时存储登录账号
这个时候可以用返回的体中的发送者账号和接收者账号来和登录账号进行判断
//服务端 #include<iostream> #include<stdio.h> #include<unistd.h> #include<netinet/in.h> #include<string.h> #include<pthread.h> #include<map> #include"protocol.h" using namespace std; //服务器 //1.接受到客户端传过来的一个结构体,这个结构体存储了账号和套接字的信息 //2.将这个信息存到一个全局的map中,这样我们就有一个简单版的数据库 //3.到目前为止客户端已经登录成功 //4.我们将A客户端传过来的信息:B的账号和要发给B的内容 //5.在这边使用B账号去从map容器查找到B的套接字,然后通过wrtie函数把数据写入到这个套接字中 //线程执行函数 int accpetfd = 0; map<int, int> myMap; void* pthread_function(void *arg) { char buf[200] = { 0 }; char readbuf[200] = { 0 }; int fd = *(int*)arg; HEAD h = { 0 }; USER u = {0}; BACKMSG back = { 0 }; CHATMSG chatmsg = { 0 }; while (1) { //这边只能read 头结构体的sizeof int res = read(fd,readbuf,sizeof(HEAD)); //我们从这个read函数读取到的readbuf是不能直接使用的,我们需要一个解密的手段 //将readbuf数组的内容通过memcpy的方式提取出来,因此我们需要先准备两个结构体 //同样的我们先读取头结构体中的数据 memcpy(&h,readbuf,sizeof(HEAD)); cout << "读取到客户端发送过来的HEAD结构体长度" << res << h.businessType << endl; //在这边我们读取到头结构体中,我们可以获取到一个业务类型的信息 //我们可以通过这个业务类型的信息来对业务类型进行判断, if (h.businessType == 1)//因为我们前面定义的1为请求登录的类型,所以我们要在这边进行登录的业务逻辑的判断 { //进来的时候我们先拿到体的数据 //这边我们要注意第三个参数,我们要使用头结构体中的 体长度 这个属性,因为这个是传过来的数据,不是本地的数据 res = read(fd,readbuf,h.businessLenght); cout << "读取到客户端发送过来的--体--结构体的长度" <<res<< endl; //我们将体结构体中的内容提取到准备好的一个USER结构体中 memcpy(&u,readbuf,sizeof(USER)); cout << "请求登录的账号是:" << u.username << endl; cout << "其对应的密码是:" << u.pass << endl; //拿到数据之后下一步就是把数据存进map容器内 //因此我们要先判断用户是否在线----容器中是否已经存在该账号 if (myMap.count(u.username) == 0)//返回0说明不在,可以插入 { myMap[u.username] = fd;//这个是同时插入键和值的方法 //-----------到目前为止我们做到了服务器这边已经同意用户登录,这个时候需要做的是服务器返回给客户端告诉用户说可以登录的一个操作 //同样的,我们这边设置1为登录成功的标志位 back.flag = 1; strcpy(back.message,"登录成功"); } else { //2为登录失败的标志位 back.flag = 2; strcpy(back.message, "登录失败"); } //我们将头结构体清空,然后重新填入一个头,这个头是登录响应的 bzero(&h, sizeof(HEAD)); h.businessType = 1;//这个依旧是登录类型 h.businessLenght = sizeof(BACKMSG); //这里一样将数据给到buf这个数组,然后通过write的方式进行 服务器对客户端的反馈 memcpy(buf,&h,sizeof(HEAD)); memcpy(buf+sizeof(HEAD),&back,sizeof(BACKMSG)); //通过write函数反馈给客户端 int res = write(fd,buf,sizeof(HEAD)+sizeof(BACKMSG)); bzero(buf,sizeof(buf)); cout << "读到客户端发送的USER结构体的数据大小res = " << res << endl; bzero(readbuf,sizeof(readbuf)); } else if (h.businessType == 2)//这个是聊天类型 { //1.拿出CHATMSG res = read(fd, readbuf, h.businessLenght); cout << "读取到客户端发送过来的--体--结构体的长度" << res << endl; //我们将readbuf中的内容提取到准备好的一个CHATMSG结构体中 memcpy(&chatmsg, readbuf, sizeof(CHATMSG)); cout << "服务器读取到" << chatmsg.sendid <<"发送:"<<chatmsg.context << " 给" << chatmsg.recvid << endl; //到现在为止,服务器可以接收到客户端的信息:A发送什么内容给B //现在我们需要做的是通过接收者的账号,从这个账号去map容器中取出对应的fd bzero(&h,sizeof(h)); h.businessType = 2;//聊天定义为2类型 h.businessLenght = sizeof(CHATMSG); memcpy(buf,&h,sizeof(HEAD)); memcpy(buf+sizeof(HEAD),&chatmsg,sizeof(CHATMSG)); //先写给发的,再写给接收的 res = write(myMap[chatmsg.recvid],buf,sizeof(CHATMSG)+sizeof(HEAD));//写给那个接收者 if (res <= 0) { perror("发送失败1"); return 0; } res = write(fd,buf,sizeof(HEAD)+sizeof(CHATMSG));//写给发送者 if (res <= 0) { perror("发送失败2"); return 0; } bzero(buf, sizeof(buf)); bzero(readbuf, sizeof(readbuf)); bzero(&h, sizeof(h)); bzero(&chatmsg, sizeof(chatmsg)); } } return NULL; } int main() { //1.网络初始化,创建套接字 int sockedfd = socket(AF_INET,SOCK_STREAM,0); if (sockedfd < 0) { perror("网络初始化失败"); return 0; } else { //2.利用bind函数绑定IP地址和端口号 //先创建一个结构体 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY;//系统默认IP addr.sin_port = htons(10002);//参数写10000以后 int len = sizeof(addr); if(bind(sockedfd,(struct sockaddr*)&addr,len)<0) { perror("bind 失败"); return 0; } //3.设置监听套接字 if (listen(sockedfd, 10) < 0) { perror("listen 失败"); return 0; } //4.到此为止说明服务器网络搭建成功了 cout << "客户端连接搭建成功" <<endl; while (1) { accpetfd = accept(sockedfd,NULL,NULL);//这个返回的值就是接上来的客户端的套接字 cout << "客户端连接成功" << accpetfd << endl; pthread_t prthreadid = 0; //这边把accpectfd传进去 if (pthread_create(&prthreadid,NULL,pthread_function, &accpetfd)!=0) { perror("线程创建失败"); return 0; } } } return 0; }
//客户端 #include<iostream> #include<sys/types.h> #include<stdio.h> #include<sys/socket.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<pthread.h> #include<netinet/in.h> #include"protocol.h" using namespace std; bool isLogin = false; int loginUser = 0; int sockedfd = 0; void* read_pthread_function(void* arg) { //读进程做读的动作 char buf[200] = { 0 }; char readbuf[200] = { 0 }; int fd = *(int*)arg; HEAD h = { 0 }; BACKMSG back = { 0 }; CHATMSG chatmsg = { 0 }; while (1) { //1.先读HEAD结构体来判断类型 int res = read(fd,readbuf,sizeof(HEAD)); memcpy(&h,readbuf,sizeof(HEAD)); if (h.businessType == 1)//这个依旧是对登录类型的业务处理,我们这边要将服务器对客户端请求登录做一个反馈 { //注意在读取体的长度时,我们都要用头结构体中属性来读取 res = read(fd,readbuf,h.businessLenght); memcpy(&back,readbuf,sizeof(BACKMSG)); cout << "登录结果:" << back.message<<endl; if (back.flag == 1) { isLogin = true; } } else if (h.businessType == 2)//这个是聊天业务的处理,我们这边要判断是发送方还是接收方 { res = read(fd,readbuf,h.businessLenght); memcpy(&chatmsg,readbuf,sizeof(CHATMSG)); if (loginUser == chatmsg.sendid) { cout << "我说:" << chatmsg.context << endl; } else { cout<<chatmsg.sendid << "对我说" <<chatmsg.context <<endl; } } bzero(&h,sizeof(HEAD)); bzero(readbuf, sizeof(readbuf)); } return NULL; } void* write_pthread_function(void* arg) { //写线程做写入的动作 //这边准备一个接收存了结构体的 数组 char buf[200] = { 0 }; int wfd = *(int*)arg; HEAD h = { 0 }; USER u = { 0 }; CHATMSG chatmsg = { 0 }; while (1) { if (isLogin == false)//未登录的情况 { //-----1.先将账号密码给到USER结构体 cout << "请输入账号" << endl; cin >> u.username; loginUser = u.username; cout << "请输入密码" << endl; cin >> u.pass; //-----2.再直接定义业务类型 h.businessType = 1; //这边我们先设定1为登录的类型 h.businessLenght = sizeof(USER);//将体的长度在这边给到HEAD结构体 //-----3.将头和体拼接在一个buf里面,这边要用到指针偏移的操作 memcpy(buf, &h, sizeof(HEAD));//我们先将 头 放进这个buf数组 memcpy(buf + sizeof(HEAD), &u, sizeof(USER));//然后再将 体 放进这个buf数组,这边要先做指针偏移,然后再放进来 //-----4.到目前为止,我们已经做到了将用户登录的信息全部都给到一个buf数组 //这个时候我们只需要和之前一样通过write函数把这个结构体给到服务器端就行 //这边我们需要注意的是在使用write这个函数的第三个参数不能是sizeof(buf) int res = write(wfd, buf, sizeof(HEAD) + sizeof(USER)); cout << "write_pthread_function wirite res = " << res << endl; bzero(buf, sizeof(buf)); } else { cout << "请先输入要聊天的那个人的ID:" << endl; cin >>chatmsg.recvid ;//先输入要聊天的对象 cout << "再输入要聊天的内容" << endl; cin >> chatmsg.context;//再输入内容 //因为发送者就是我们这边登录的账号,因此可以直接赋值 chatmsg.sendid = u.username; //同样的先清空头 bzero(&h,sizeof(HEAD)); //再重新填充头的信息 h.businessType = 2;//2就代表聊天的类型 h.businessLenght = sizeof(CHATMSG); //然后将头和体拼接到buf里面 memcpy(buf,&h,sizeof(HEAD)); memcpy(buf+sizeof(HEAD),&chatmsg,sizeof(CHATMSG)); int res = write(wfd,buf, sizeof(HEAD)+sizeof(CHATMSG)); bzero(buf, sizeof(buf)); } sleep(3); } return NULL; } int main() { sockedfd = socket(AF_INET,SOCK_STREAM,0); char buf[30] = { 0 }; struct sockaddr_in addr; if (sockedfd < 0) { perror("客户端的网络初始化失败(套接字创建失败)"); return 0; } else { addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("192.168.43.128"); addr.sin_port = htons(10002); int len = sizeof(addr); if (connect(sockedfd,(struct sockaddr*)&addr,len)==-1) { perror("客户端连接服务器失败"); return 0; } cout << "客户端 " << sockedfd << " 成功连接服务器" << endl; //链接成功之后第一件事情就是开线程 //这边我们开一个读取线程,开一个写入线程 pthread_t read_prthreadid = 0; if (pthread_create(&read_prthreadid, NULL, read_pthread_function, &sockedfd) != 0) { perror("线程创建失败"); return 0; } pthread_t write_prthreadid = 0; if (pthread_create(&write_prthreadid, NULL, write_pthread_function, &sockedfd) != 0) { perror("线程创建失败"); return 0; } while (1) { } } return 0; }
//定长包头+不定长包体 #pragma once //-----包头协议结构体----- typedef struct head { int businessType;//业务标识 int businessLenght;//包体长度 }HEAD; //-----业务包体协议结构体 //登录请求包体 typedef struct userinfo { int username; char pass[20]; }USER; //登录返回包体 typedef struct backmsg { int flag;//失败的情况有好几种 char message[50]; }BACKMSG; //聊天请求包体 typedef struct chatmsg { int sendid; int recvid; char context[150]; }CHATMSG;