TCP套接字通信流程

服务器端:

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转接:
相较于多线程/ 多进程的优势:

  • 将一部分监听的工作, 从应用程序移交给了内核
    • 连接请求 -> 检测读缓冲区是否有数据 (监听的fd)
      • accept
    • 接收数据 -> 检测读缓冲区是否有数据 (通信的fd)
      • read/recv
    • 发送数据 -> 检测写缓冲区是否有数据 (通信的fd)
      • 写事件触发的时机问题
        • 如果缓冲区可写

如何实现多路IO转接:

  • select
    • 跨平台
    • 检测最大数据量: 1024
    • 线性检测, 检测的fd越多, 效率越低
  • poll
    • linux
    • 检测的最大数据量可以超过1024
    • 线性检测, 检测的fd越多, 效率越低
  • epoll
    • linux
    • 检测的最大数据量可以超过1024
    • 树状检测
// 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

2.2 客户端

优化方式: 提高单位时间内客户端发送数据的数量

  1. 第1种方式 -> 单线程/单进程

    for(int i=0; i
  2. 第2种方式 -> 多线程, 多进程

    1. 主线程
    	- 创建子线程
    2. 子线程 - 每个线程的内部操作
    	for(int i=0; i
  3. 第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();
    

3 C++套接字类

3.1 设计思路 - 版本1

第一个版本的基本设计思路, 以下是提供的供参考的伪代码:

  1. 服务器

    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去识别该客户端到底是哪一个需要一定的工作量
    
  2. 客户端

    class TcpClient
    {
    public:
    	TcpClient();
    	// 创建套接字, 也可以通过构造函数实现
    	int initClient(...);
    	// 等待并接受连接请求
    	int connectServer(...);
    	// 接收数据
    	int recvMsg(...);
    	// 发送数据
    	int sendMsg(...);
    	// 关闭套接字
    	void closefd();
    private:
    	int m_connfd;
    };
    
    每一个TcpClient对象就相当于是 客户端
    c语言中每个客户端是一个文件描述符
    c++中每个客户端是一个对象
    	- 属性: 通信的文件描述符
    	- 成员方法:
    		- 初始化
    		- 连接
    		- 通信
    		- 销毁资源
    

3.2 设计思路 - 版本2

以下是版本2 的设计思路, 参考代码如下:

  1. 服务器

    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();
        }
    
  2. 客户端

    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;		// 用于通信的套接字
    };
    

你可能感兴趣的:(基础知识)