基于阿里云用C/C++做了一个http协议与TCP协议的web聊天室的服务器——《干饭聊天室》
在这里首先感谢前端小伙伴飞鸟
前端技术请看一款基于React、C++,使用TCP/HTTP协议的多人聊天室小应用
如有错误,欢迎各位批评指正
本文一切输出内容都是调试用的
一、用cpp封装socket套接字
包含成员有位于in.h下的sockaddr_in结构体:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family 地址结构类型*/
__be16 sin_port; /* Port number 端口号*/
struct in_addr sin_addr; /* Internet address IP地址*/
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
以及成员unistd.h下的socklen_t。
封装函数socket(),bind(),listen(),accept(),connect()以及对结构体sockaddr_in成员赋值的函数
class net{
public:
sockaddr_in server;
socklen_t len;
public:
net();
~net();
int Socket(int Domain,int Type,int protocol);
void Bind(int Socketfd,const struct sockaddr *addr,socklen_t addrlen);
void Listen(int Socketfd,int backlog);
int Accept(int Socketfd,struct sockaddr *addr,socklen_t *addrlen);
void Connect(int Socketfd,const struct sockaddr *addr,socklen_t addrlen);
void set_sockaddr_in(int Domain,char *ip,int port);
};
二、封装epoll函数实现并发服务器——多任务IO服务器
包含epoll_create(),epoll_ctl(),epoll_wait()
class Epoll{
public:
struct epoll_event e_event[MAXSIZE];
public:
Epoll();
~Epoll();
int Epoll_create(int size);
void Epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
int Epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
};
三、封装http协议
这里首先要先了解http协议,这里参考的几篇文章:
遇到的坑:
"Access-Control-Allow-Origin:*\r\n" //解决前后端分离时浏览器跨域问题
"Connection:keep-alive\r\n" //前后端保持长链接,不总是很稳定,TCP有时会自动断开
struct _client{
char name[10];
char pwd[18];
char _hash[32];
int client_fd;
std::vector<std::string> chat_record;
}Client;
上述结构体用来存储处理的这次http报文中的姓名,密码,socket文件描述符还有该用户在聊天中未收到的聊天记录。每次用户登陆都会在std::unordered_map<std::string,_client> data_client;//客户数据
创建出以姓名为建值的哈希表,value为_client类型用来存储token值及hash还有聊天记录。
"Content-Length:"; //响应头通知浏览器接收多少个字节。
响应头在这里并未写全因为每次响应数据的长度即大小不知道需要发送之前计算。
#ifndef __HTTP_H_
#define __HTTP_H_
#include<iostream>
#include<string.h>
#include<unordered_map>
#include<string>
#include"NetBYdw.h"
#include<fcntl.h>
#include<stdlib.h>
#include <sys/stat.h>
#include<vector>
//响应头
const char success[]="HTTP/1.1 200 ok\r\n"
"Connection:keep-alive\r\n"
"Access-Control-Allow-Origin:*\r\n" //解决前后端分离时浏览器跨域问题
//"Access-Control-Allow-Origin:http://47.110.144.145:80\r\n"
"Access-Control-Allow-Methods:POST,GET,OPTIONS,DELETE\r\n"
"Access-Control-Max-Age:3600\r\n"
"Access-Control-Allow-Headers:x-requested-with,content-type\r\n"
"Access-Control-Expose-Headers:serve-header\r\n"
"Content-Length:"; //响应头通知浏览器接收多少个字节。
class http{
private:
char login_status[16];
struct _client{
char name[10];
char pwd[18];
char _hash[32];
int client_fd;
std::vector<std::string> chat_record;
}Client;
public:
char Request_method[8];//请求方法
char Request_data[1024];//报文整体,相当于in_str
char send_data[1024];//需要发送的报文数据
char Newspaper_data[1024];//报文体
std::unordered_map<std::string,std::string> data_map;//报文真实数据
std::unordered_map<std::string,_client> data_client;//客户数据
public:
http();
~http();
char * http_Request_method(char in_str[],char out_str[]);//
char * http_get_Newspaper(char in_str[],char out_str[]);//
int http_send(char out_str[]);//
int http_put(int client_fd);//处理put请求
int http_post(int client_fd);//处理post请求
int http_get(int client_fd);//处理get请求
int http_get_data(char in_str[]);//
int http_delete(char in_str[]);
int http_get_original_news(char in_str[]);
};
#endif
http协议的处理
前面介绍到响应头不全,下面就介绍了为什么不全itoa(strlen(str),c_size,10);
把字节大小改为字符串,再通过write(client_fd,"\r\n\r\n",strlen("\r\n\r\n"));
实现整的http响应头。
char str[]=R"({"delete":0})";
char c_size[1024];
memset(c_size,0,sizeof(c_size));
itoa(strlen(str),c_size,10);
printf("%s\r\n\r\n",c_size);
write(client_fd,c_size,strlen(c_size));
write(client_fd,"\r\n\r\n",strlen("\r\n\r\n"));
write(client_fd,str,strlen(str));
无序哈希表问题
this->data_map.insert(std::make_pair(first.c_str(),second.c_str()));//必须这么存才能在哈希表里找到,不能存对象``在这里存数据时开始使用的是char *类型的字符串,但发现虽然是C语言字符串,但键值类型还是地址,比如char *first=“name“,查找时查找data_map["name"]是找不到的判断该键值不存在。后来改为:unordered_map\<std::string,std::string\>类型,又发现把string类型当键值也找不到,最后发现采用string.c_str()类型可以成功存储。欢迎大神解答这个问题。
int http::http_get_data(char in_str[]){
this->data_map.clear();
for(int i=0;in_str[i]!='\0';++i){
//把报文数据改变成哈希表,=号前为键值,=号后为value
std::string first;
for(int j=0;j<1024;++j){
if(in_str[i]=='\0'){
return -1;
}
if(in_str[i]!='='){
first.push_back(in_str[i]);
++i;
}else {
first.push_back('\0');
++i;
break;
}
}
std::string second;
for(int j=0;j<1024;++j){
if(in_str[i]!='&'&&in_str[i]!='\0'){
second.push_back(in_str[i]);
++i;
}else {
second.push_back('\0');
break;
}
}
this->data_map.insert(std::make_pair(first.c_str(),second.c_str()));//必须这么存才能在哈希表里找到,不能存对象
}
return 0;
}
四、书写server服务端即main程序
如果服务器先关闭,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
解决服务端先关闭办法:
const int reuse = 1;
setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));//解决服务器先关闭
为了在服务器端的后台长期运行,创建守护进程,代码如下:
int r_ret=fork();
if(r_ret==0){
setsid();
}else{
sleep(1);
exit(1);
}
守护进程的简单创建:
守护进程的简单创建: |
---|
1、创建子进程,父进程退出所有工作在子进程中进行形式上脱离了控制终端 |
2、在子进程中创建新会话setsid()函数使子进程完全独立出来,脱离控制 |
setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。 pid_t setsid(void); 成功:返回调用进程的会话ID;失败:-1,设置errno 调用了setsid函数的进程,既是新的会长,也是新的组长。
接下来需要了解TCP的连接方式,三次握手四次挥手,可以参考HTTP详解的评论区,接下来我也会写。tcp连接的建立。
int server_fd,client_fd,epfd;
int client[MAXSIZE];
int flag1=0,flag2=0;
std::unordered_map<int,_client> client_map;
socklen_t server_len,client_len;
net Server;
sockaddr_in Client;
struct epoll_event tev;
Epoll _epoll;
http _http;
Server.set_sockaddr_in(AF_INET,ip,htons(port));
server_fd=Server.Socket(AF_INET,SOCK_STREAM,0);
const int reuse = 1;
setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));//解决服务器先关闭
Server.Bind(server_fd,(struct sockaddr *)&Server.server,sizeof(Server.server));
Server.Listen(server_fd,128);
client_fd=Server.Accept(server_fd,(struct sockaddr *)&client,&client_len);
采用epoll实现高并发
不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件
。epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,
只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
http的处理
char __str[1024];
memset(__str,0,sizeof(__str));
int __ret=read(client_fd,__str,sizeof(__str));//读客户端文件描述符client_fd里的内容
if(__ret<0){
perror("post read error\n");
}
printf("%s\n",__str);
_http.http_get_original_news(__str);//获取原始报文数据
_http.http_Request_method(_http.Request_data,_http.Request_method);//获取请求方法
if(strcmp(_http.Request_method,"GET")==0){
//请求方法
_http.http_get(client_fd);//具体方法处理
memset(_http.Request_method,0,sizeof(_http.Request_method));//清除本次请求方法
memset(_http.Request_data,0,sizeof(_http.Request_data));//清除本次原始报文
}else if(strcmp(_http.Request_method,"PUT")==0){
_http.http_put(client_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"POST")==0){
_http.http_post(client_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"Delete")==0){
}else{
}
#include"NetBYdw.h"
#include"Net_epoll.h"
#include "sign_in.h"
#include <unordered_map>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "http.h"
#include<sys/sendfile.h>
#define port 4567
char ip[]="***.***.***.***"; //服务器内网ip
//char ip[]="172.16.10.66";//本地调试接口
struct _client{
int fd_id;
int flag;
char name[1024];
};
int main(int argc,char *argv[]){
int r_ret=fork();
if(r_ret==0){
setsid();
}else{
sleep(1);
exit(1);
}
int server_fd,client_fd,epfd;
int client[MAXSIZE];
int flag1=0,flag2=0;
std::unordered_map<int,_client> client_map;
socklen_t server_len,client_len;
net Server;
sockaddr_in Client;
struct epoll_event tev;
Epoll _epoll;
http _http;
Server.set_sockaddr_in(AF_INET,ip,htons(port));
server_fd=Server.Socket(AF_INET,SOCK_STREAM,0);
const int reuse = 1;
setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));//解决服务器先关闭
Server.Bind(server_fd,(struct sockaddr *)&Server.server,sizeof(Server.server));
Server.Listen(server_fd,128);
// client_fd=Server.Accept(server_fd,(struct sockaddr *)&client,&client_len);
int i=0;
for(i=0;i<MAXSIZE;++i)
client[i]=-1;
int _addr=-1;
epfd=_epoll.Epoll_create(MAXSIZE);
tev.events=EPOLLIN;
tev.data.fd=server_fd;
_epoll.Epoll_ctl(epfd,EPOLL_CTL_ADD,server_fd,&tev);
while (1){
int ret;
ret=_epoll.Epoll_wait(epfd,_epoll.e_event,MAXSIZE,0);
for(i=0;i<ret;++i){
if((EPOLLIN&_epoll.e_event[i].events)==0)
continue;
if(_epoll.e_event[i].data.fd==server_fd){
client_fd=Server.Accept(server_fd,(struct sockaddr*)&Client,&client_len);
char str[64];
std::cout<<"received from "<<inet_ntop(AF_INET,&Client.sin_addr.s_addr,str,sizeof(str))<<" at PORT"<<ntohs(Client.sin_port)<<std::endl;
//把建立连接的客户端文件描述符写入clinet数组
int j;
for(j=0;i<MAXSIZE;++j){
if(client[j]<0){
client[j]=client_fd;
break;
}
}
//判断是否达到连接上限
if(j==MAXSIZE){
std::cout<<"client is full"<<std::endl;
break;
}
client_map.insert(std::pair<int,_client>(client_fd,{
client_fd,0,0}));
//把指向栈顶的指针_addr移位
if(j>_addr) _addr=j;
//把得到的客户端文件描述符写入epfd中
tev.events=EPOLLIN;
tev.data.fd=client_fd;
_epoll.Epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&tev);
//登陆界面
char __str[1024];
memset(__str,0,sizeof(__str));
int __ret=read(client_fd,__str,sizeof(__str));
if(__ret<0){
perror("post read error\n");
}
printf("%s\n",__str);
_http.http_get_original_news(__str);
_http.http_Request_method(_http.Request_data,_http.Request_method);
if(strcmp(_http.Request_method,"GET")==0){
_http.http_get(client_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"PUT")==0){
_http.http_put(client_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"POST")==0){
_http.http_post(client_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"Delete")==0){
}else{
}
}else{
int read_fd=_epoll.e_event[i].data.fd;
int ret_sum;
char buf[1024];
memset(buf,0,sizeof(buf));
ret_sum=read(read_fd,buf,sizeof(buf));
if(ret_sum<0){
perror("read error");
}else if(ret_sum==0){
//判断TCP是否断开连接
char str[64];
std::cout<<inet_ntop(AF_INET,&Client.sin_addr,str,sizeof(str))<<" is closed"<<std::endl;
int j=0;
for(j=0;j<MAXSIZE;++j){
if(client[j]==read_fd){
client[j]=-1;
break;
}
}
client_map.erase(read_fd);
_epoll.Epoll_ctl(epfd,EPOLL_CTL_DEL,read_fd,_epoll.e_event);
close(read_fd);
}else{
printf("%s\n",buf);
_http.http_get_original_news(buf);
_http.http_Request_method(_http.Request_data,_http.Request_method);
memset(buf,0,sizeof(buf));
if(strcmp(_http.Request_method,"GET")==0){
_http.http_get(read_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"PUT")==0){
_http.http_put(read_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"POST")==0){
_http.http_post(read_fd);
memset(_http.Request_method,0,sizeof(_http.Request_method));
memset(_http.Request_data,0,sizeof(_http.Request_data));
}else if(strcmp(_http.Request_method,"Delete")==0){
}else{
}
}
}
}
}
close(server_fd);
close(epfd);
return 0;
}
五、数据存储方式
目前采用的文件存储的方式。包括聊天记录,用户,密码。
六、未来计划
打算加入线程池实现高并发,数据存储打算采用Redis数据库。
七、服务器重启shell
#!/bin/bash
id=`ps aux|awk '{print $2 $11}'|grep ./server|awk 'BEGIN{FS="."}{print $1}'`
kill -9 ${id}
if [ $? -eq 0 ]; then
echo "success!"
else
echo "failed!"
fi
./server
八、源码连接
https://github.com/dwnb/chat_web
https://github.com/lzxjack/chat-room
转载:https://blog.csdn.net/asdasdde/article/details/117453045