服务器端:
1. 创建监听套接字
int socket(int domain, int type, int protocol);
2. 将监听的套接字和本地的IP和端口绑定
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
3. 设置监听
int listen(int s, int backlog);
4. 等待并接受连接请求 -> 默认阻塞函数
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
- 参数
- s: 监听的套接字
- addr: 传出, 保存的是连接的客户端的IP和端口信息
- addrlen: addr的长度
- 返回值: 和已经建立连接的客户端通信的文件描述符
5. 通信
- 接收数据: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 发送数据: int send(int s, const void *msg, size_t len, int flags);
6. 关闭套接字
- 通信的套接字: close(connfd);
- 监听的套接字: close(lfd);
服务器端使用的文件描述符有两类:
- 监听(一个)
- 通信(>=1个)
服务器被动接受连接的角色
客户端:
1. 创建通信的套接字
int socket(int domain, int type, int protocol);
2. 连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3. 通信
- 接收数据: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 发送数据: int send(int s, const void *msg, size_t len, int flags);
4. 关闭连接
close(fd);
主动建立连接的角色
通信效率概念:单位时间内服务器和客户端传递的数据的数量
一) 原始数据传输方式:
// 唯一的线程/进行中进行处理
while(1){
// 接受请求
accept();
// 处理请求
// 1. 接收数据
// 2. 处理数据
// 3. 发送数据
}
效率太低
二) 多进程、多线程处理方式:
缺点: 如果连接的客户端很多, 创建的线程/进程是有上限的, 有可能不能满足需求, 效率不高, 适用于客户端比较少的情况。
while(1){
// 主线程/主进程
accept();
// 和已经连接的客户端的通信操作, 交给对应的子线程和子进程处理
// 创建子进程/子线程
// 1. 接收数据
// 2. 处理数据
// 3. 发送数据
}
三) 多路IO转接:
相较于多线程/ 多进程的优势:
如何实现多路IO转接:
// epoll举例
1. int epfd = epoll_create(int size);
2. 添加要检测的文件描述符 - lfd(监听的)
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3. 开始检测
int res = epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
4. 根据返回值判断
for(int i=0; i<res; ++i){
// 1. 根据events中事件做判断
if(读事件){
if(新连接){
// 从events取出fd
// 1. 有新连接
int connfd = accept();
// 1.1 connfd 上树
} else{
// 2. 有数据到达(连接已经建立)
int ret = read();
// 2.1 数据处理
}
}
}
四) 多路IO转接 + 多线程(线程池)
for(int i=0; i
优化方式: 提高单位时间内客户端发送数据的数量
第1种方式 -> 单线程/单进程
for(int i=0; i
第2种方式 -> 多线程, 多进程
1. 主线程
- 创建子线程
2. 子线程 - 每个线程的内部操作
for(int i=0; i
第3种方式 -> 连接池
连接池: 有一个数据结构, 存储了一些可以直接用于通信的fd, 这个数据结构称之为连接池
如何使用:
1. 在通信之前, 先创建若干个连接(fd), 将其存储到 vector
queue v;
for(int i=0; i fd
- 需要进行线程同步
m_mutex.lock()
fd = v.front();
v.pop();
m_mutex.unlock();
2). 使用拿到的fd进行通信
3). 通信完成, 将fd放回到连接池中
- 需要进行线程同步
m_mutex.lock()
fd = v.push();
m_mutex.unlock();
第一个版本的基本设计思路, 以下是提供的供参考的伪代码:
服务器
class TcpServer
{
public:
TcpServer();
// 初始化套接字(绑定+监听), 也可以通过构造函数实现
int initServer(...);
// 等待并接受连接请求
int accetpServer(...);
// 接收数据
int recvMsg(...);
// 发送数据
int sendMsg(...);
// 关闭套接字
void closefd();
private:
int m_lfd; // 监听的套接字
int m_connfd; // 通信的套接字
vector m_fds; // 改进之后
};
问题: 只能满足一个客户端连接, 改进之后, 虽然能管理多个客户端, 但是通过fd去识别该客户端到底是哪一个需要一定的工作量
客户端
class TcpClient
{
public:
TcpClient();
// 创建套接字, 也可以通过构造函数实现
int initClient(...);
// 等待并接受连接请求
int connectServer(...);
// 接收数据
int recvMsg(...);
// 发送数据
int sendMsg(...);
// 关闭套接字
void closefd();
private:
int m_connfd;
};
每一个TcpClient对象就相当于是 客户端
c语言中每个客户端是一个文件描述符
c++中每个客户端是一个对象
- 属性: 通信的文件描述符
- 成员方法:
- 初始化
- 连接
- 通信
- 销毁资源
以下是版本2 的设计思路, 参考代码如下:
服务器
class TcpServer
{
public:
TcpServer();
~TcpServer();
// 服务器设置监听(创建+绑定+监听)
int setListen(unsigned short port);
// 等待并接受客户端连接请求, 默认连接超时时间为10000s
TcpSocket* acceptConn(int timeout = 10000);
void closefd();
private:
int m_lfd; // 用于监听的文件描述符
};
TcpSocket* acceptConn(int timeout = 10000);
- 接收连接请求, 直接得到一个可以通信的套接字对象
接收多个客户端的连接
TcpServer server;
while(1)
{
TcpSocket* socket = server.acceptConn();
}
客户端
class TcpSocket
{
public:
TcpSocket();
// 使用一个可以用于通信的套接字实例化套接字对象
TcpSocket(int connfd);
~TcpSocket();
// 连接服务器
int connectToHost(char* ip, unsigned short port, int timeout = TIMEOUT);
// 发送数据
int sendMsg(char* sendData, int dataLen, int timeout = TIMEOUT);
// 接收数据
int recvMsg(char** recvData, int &recvLen, int timeout = TIMEOUT);
// 断开连接
void disConnect();
// 释放内存
void freeMemory(char** buf);
private:
int m_socket; // 用于通信的套接字
};