Based on Alibaba Cloud, I made a web chat room server with http protocol and TCP protocol in C/C++-"Danfan Chat Room"

Based on Alibaba Cloud, I made a web chat room server with http protocol and TCP protocol in C/C++-"Danfan Chat Room"

First of all, I would like to thank the front-end partner Flying Bird. For the
front-end technology, please see a small multi-person chat room application based on React, C++ and using the TCP/HTTP protocol.
如有错误,欢迎各位批评指正
All the output in this article is for debugging.

Insert picture description here
Insert picture description here

One, use cpp to encapsulate the socket socket

Containing members are the sockaddr_in structure under in.h:

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)];
};

And socklen_t under member unistd.h.

Encapsulation functions socket(), bind(), listen(), accept(), connect() and functions to assign values ​​to the sockaddr_in members of the structure

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);
};

Two, encapsulate the epoll function to implement a concurrent server-multi-task IO server

Contains 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);
};

Three, encapsulate the http protocol

First of all, we must first understand the http protocol. Here are a few articles for reference:

HTTP detailed

Novice HTTP Tutorial

Insert picture description here
Insert picture description here

Pit encountered:

"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;

The above structure is used to store the name, password, socket file descriptor and chat records that the user has not received in the chat in the HTTP message processed. Every time a user logs in, std::unordered_map<std::string,_client> data_client;//客户数据a hash table is created with the name as the value, and the value is the _client type to store the token value, hash, and chat history.

"Content-Length:"; //响应头通知浏览器接收多少个字节。The response header is not written here, because the length or size of the response data each time is not known and needs to be calculated before sending.

#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 protocol processing

The incomplete response header was introduced in the previous section. The following explains why itoa(strlen(str),c_size,10);the byte size is not changed to a string, and then the write(client_fd,"\r\n\r\n",strlen("\r\n\r\n"));entire http response header is realized.

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));

Unordered hash table problem

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;
}

Fourth, write the server server that is the main program

如果服务器先关闭,虽然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端口。Solve the method of closing the server first:

const int reuse = 1;            
setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));//解决服务器先关闭

In order to run in the background on the server side for a long time, create a daemon process, the code is as follows:

int r_ret=fork();
if(r_ret==0){
    setsid();
}else{
    sleep(1);
    exit(1);
}

Simple creation of the daemon:

Simple creation of the daemon:
1. Create a child process, the parent process exits all work in the child process formally separated from the control terminal
2. Create a new session setsid() function in the child process to make the child process completely independent and out of control

setsid function

创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。 pid_t setsid(void); 成功:返回调用进程的会话ID;失败:-1,设置errno 调用了setsid函数的进程,既是新的会长,也是新的组长。

Next, you need to understand the TCP connection method, three-way handshake and four waved hands, you can refer to the comment area of HTTP detailed explanation , and I will write next. The establishment of a tcp connection.

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);

Use epoll to achieve high concurrency

不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件. Epoll is an enhanced version of the multiplexed IO interface select/poll under Linux. It can significantly improve the system CPU utilization when the program is only a few active in a large number of concurrent connections, because it reuses a set of file descriptors for delivery As a result, instead of forcing the developer to re-prepare the file descriptor set to be listened to each time before waiting for an event, 另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,just traverse the set of descriptors that are asynchronously awakened by the kernel IO event and added to the Ready queue.

http processing

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;
}

Five, data storage method

The current file storage method. Including chat history, user, password.

Six, future plans

Plans to join the thread pool to achieve high concurrency, and plan to use Redis database for data storage.

Seven, server restart 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

Eight, source code connection

https://github.com/dwnb/chat_web

https://github.com/lzxjack/chat-room