通信
3.服务器类的封装
mkdir chat_server
cd chat_server
main.cpp
#include <iostream>
#include "server.h"
int main()
{
//创建服务器对象
Server s (IP, PORT);
return 0;
}
server.h
#ifndef SERVER_H
#define SERVER_H
#include <stdio.h>
#include <stdlib.h>
#include <event.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <event2/listener.h>
#include <iostream>
#include <thread>
#define IP "192.168.24.149"
#define PORT 8000
class Server
{
private:
struct event_base *base; //事件集合,这个管理集合
struct evconnlistener *listener; //监听事件
private:
static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *arg);
static void client_handle(int fd);
static void read_cb(struct bufferevent *bev, void *ctx);
static void event_cb(struct bufferevent *bev, short what, void *ctx);
public:
Server(const char * ip = "127.0.0.1" , int port = 8000);
~Server();
};
#endif
server.cpp
#include "server.h"
Server::Server(const char *ip , int port)
{
//创建集合
base = event_base_new();
//监听的端口
struct sockaddr_in server_addr;
memset(&server_addr, 0 , sizeof(server_addr));//情空
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(port);
//绑定,这里实现监听连接,当有连接的时候,这个函数内部就实现了accept,同时调用listener_cb去处理连接的socket信息
//listener_cb设置成私有静态,防止被其他文件修改和引用,只在当前文件有效
listener = evconnlistener_new_bind(base, listener_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 10,
(struct sockaddr *)&server_addr, sizeof(server_addr));
if (listener == NULL)
{
std::cout << "evconnlistener_new_bind error \n" << std::endl;
}
//监听集合 客户端是否有数据发送过来
event_base_dispatch(base);
//释放
event_base_free(base);
evconnlistener_free(listener);
}
//处理socket数据,当有客户端发起连接时会调用这个函数
void Server::listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *arg)
{
std::cout << "客户端 " << fd << " 已连接 \n" << std::endl;
//创建工作线程来处理客户端
std::thread client_thread(client_handle, fd);
client_thread.detach();
}
Server::~Server()
{
}
//线程函数一般没有返回值
void Server::client_handle(int fd)
{
//创建集合,这个是工作集合
struct event_base * base = event_base_new();
//创建bufferevent对象,并与base绑定
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
if (bev == NULL)
{
printf("bufferevent_socket_new error \n");
}
//给bufferevent设置回调函数
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
//使能回调函数
bufferevent_enable(bev,EV_READ);
//监听
event_base_dispatch(base);
//释放
event_base_free(base);
bufferevent_free(bev);
}
//读取数据,客户端发送过来的数据,注册,登录那些都在这里写
void Server::read_cb(struct bufferevent *bev, void *ctx)
{
char buf[1024] = {0};
int size = bufferevent_read(bev, buf, sizeof(buf));
if (size < 0)
{
std::cout << "bufferevent_read error \n" << std::endl;
}
else
{
std::cout << "传过来的数据:" << buf << std::endl;
}
/*
数据的传送采用josn的格式,后面下面进行解析数据,然后
进行实现不同的功能,注册登录那些
*/
}
//异常
void Server::event_cb(struct bufferevent *bev, short what, void *ctx)
{
}
知识点1 - - htons() inet_addr() 网络字节序
inet_addr()
函数用于将点分十进制的 IPv4 地址转换为网络字节序的整数形式。参数 "127.0.0.1"
表示本地回环地址,即指向本机的地址。
inet_addr()
返回一个 in_addr_t
类型的值,表示转换后的 IP 地址。这个值将被赋给 server_addr.sin_addr.s_addr
字段,用于指定服务器的 IP 地址。
htons()
是一个函数,用于将主机字节序的 16 位无符号整数转换为网络字节序(大端字节序)。在这里,htons(8000)
将端口号 8000
转换为网络字节序,并赋值给 server_addr.sin_port
字段,用于指定服务器的端口。
网络字节序(Network Byte Order)是一种在计算机网络中约定的数据传输顺序,用于确保不同主机之间的通信能够正确进行。
在计算机中,数据在内存中以字节为单位存储。字节可以被划分为更小的单元,例如比特(bit)和字(word)。而字节序指的是多字节数据在内存中的存储顺序。
大端字节序(Big Endian)是指数据的高位字节存储在低地址处,低位字节存储在高地址处。例如数字 256 在大端字节序下以两个字节存储为 01 00
,其中高位字节 01
存储在低地址处,低位字节 00
存储在高地址处。
小端字节序(Little Endian)则是相反的,数据的低位字节存储在低地址处,高位字节存储在高地址处。同样以数字 256 为例,在小端字节序下以两个字节存储为 00 01
,其中低位字节 00
存储在低地址处,高位字节 01
存储在高地址处。
为了在网络中实现跨平台的数据传输,通信双方需要一致地遵循相同的字节序。这就是网络字节序的作用。在网络字节序中,使用固定的大端字节序(也称为网络序)来表示数据。
因此,在进行网络通信时,需要特别关注字节序的转换。当数据从**主机字节序(通常是小端字节序)**转换为网络字节序时,需要使用函数 htons()
、htonl()
等进行转换。而在接收到网络字节序数据后,需要使用函数 ntohs()
、ntohl()
等将其转换为主机字节序。
数据在内存中是有存储顺序的,在主机当中通常是小端字节序,网络通信中,网络字节序是大端字节序,也就是数据要进行网络传输时要从主机的小端字节序转换成大端字节序的网络字节序。
知识点2 - - fd
"fd"
是指文件描述符(File Descriptor),它是操作系统用来标识已打开文件或者资源的整数值。在类Unix系统中,包括Linux和Mac OS等,文件描述符是对文件、管道、套接字等资源的引用。
文件描述符通常是非负整数,每个进程都有一个文件描述符表,其中存储了已打开文件的相关信息。当打开一个文件时,操作系统会分配一个文件描述符,并将其返回给应用程序。应用程序可以通过文件描述符来进行对文件的读写、关闭等操作。
标准输入、标准输出和标准错误输出分别用文件描述符 0、1 和 2 来表示。其他的文件描述符一般从 3 开始递增分配。
例如,当你在程序中使用 open()
函数打开一个文件时,该函数会返回一个文件描述符,你可以使用该文件描述符来访问和操作该文件。同样,当你使用套接字进行网络通信时,也会获得一个文件描述符来代表该套接字。
就是一个整形的返回值,是非负的,没有什么意义,用来区分不同的连接
**知识点3 - - 为什么listener_cb 设置为 static **
将 listener_cb
设置为 static 是**为了限制其作用域,使其只在当前文件中有效,并且不能被其他文件引用**。这样做主要有以下几个原因:
- 封装性:将
listener_cb
设置为 static 可以将其作为 Server 类的私有成员函数,防止外部其他文件直接使用或修改该函数。 - 函数指针要求:
listener_cb
是作为参数传递给evconnlistener_new_bind()
函数的回调函数。回调函数是通过函数指针来指定的,而函数指针的类型是根据函数声明和定义的可见范围确定的。如果不将listener_cb
设置为 static,它的作用域将扩展到整个编译单元(文件),可以被其他文件引用,从而可能导致类型不匹配的问题。 - 代码安全性:将
listener_cb
设置为 static 可以在一定程度上降低代码的安全风险。如果listener_cb
不被其他文件引用,那么其他文件也无法直接修改listener_cb
,避免了潜在的不可预测行为或错误。