🌎 Tcp协议Socket编程
本次socket编程需要使用到 日志文件,此为具体日志编写过程。以及 线程池,线程池原理比较简单,看注释即可。
文章目录:
Tcp协议Socket编程
TCP Socket API简介
构建Tcp_echo_server
TcpServer服务器端
服务器循环控制
Service服务
Tcp客户端
Version 0:单进程服务
Version 1: 多进程服务
Version 2: 多线程服务
Version 3: 线程池服务
🚀TCP Socket API简介
int socket(int domain, int type, int protocol);
socket接口打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符。应用程序可以像读写文件一样用 read/write 在网络上收发数据。
- domain参数:代表协议族,决定了套接字使用的协议类型。对于 IPv4, family 参数指定为 AF_INET。
- protocol参数:protocol 参数指定为 0 即可。
- type参数:对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议。
- 返回值: 如果 socket()调用出错则返回-1, 成功返回文件描述符。
int listen(int sockfd, int backlog);
listen()声明sockfd正处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, backlog设置不会太大(一般是 5), 具体细节以后再说。
- sockfd参数:文件描述符。
- 返回值:listen()成功返回 0,失败返回-1;
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
三次握手完成后, 服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
- sockfd参数:文件描述符。
- addr参数: 是一个输出型参数,accept()返回时传出客户端的地址和端口号,如果给 addr 参数传 NULL,表示不关心客户端的地址。
- addrlen参数: 是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
int connect(int sockfd, struct sockaddr* addr, socklen_t addrlen);
客户端需要调用 connect()连接服务器。
- connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址。
- connect()成功返回 0,出错返回-1。
🚀构建Tcp_echo_server
TcpSocket编程并不是很需要我们非常理解tcp协议的原理以及实现方式方法。要实现TcpSocket编程与UdpSocket相同,都需要客户端与服务器端,服务器端实现具体的协议通信类型及方法,客户端实现对任务的调用等工作。
✈️TcpServer服务器端
首先构建TcpServer服务器端,在TcpServer.hpp内写一个名为 TcpServer 的类:
const static int defaultsockfd = -1;class TcpServer
{
public:TcpServer(uint16_t port):_port(port), _listensock(defaultsockfd){}void InitServer()// 初始化服务器{}void Loop()// 服务器是个死循环{}~TcpServer(){}
private:uint16_t _port;// 端口号int _listensock;// 文件描述符
};
我们知道,服务器没有被要求停下时是一直在运行的,所以在类内我们需要实现一个Loop函数,用来维持服务器的稳定。
在初始化阶段与UdpServer类似,首先创建tcp的套接字,我们使用socket()接口创建,对返回值进行接收并判断是否合法,接着就是绑定端口和ip,绑定之前需要首先填充sockaddr_in 结构体信息字段,最后进行绑定:
// 网络中错误类型枚举,类外实现
enum
{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR,
};void InitServer()
{_listensock = ::socket(AF_INET, SOCK_STREAM, 0);if(_listensock < 0){LOG(FATAL, "socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, sockfd is: %d", _listensock);//填充套接字信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_listensock, (struct sockaddr*)&local, sizeof(local));if(n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}LOG(DEBUG, "bind success, sockfd is: %d", _listensock);
}
在UdpServer类内绑定完成后初始化工作就完成了,但是对于TcpServer来说,因为Tcp是面向连接的,所以在通信之前,必须得首先建立连接,而服务器将来则是被连接的,启动后未来则会一直等待客户端连接,所以我们使用listen()接口进行对客户端的监听,其中listen()接口的第二个参现在我们不需要关心,将其设置为全局变量即可:
const static int gbacklog = 16;void InitServer()
{_listensock = ::socket(AF_INET, SOCK_STREAM, 0);if(_listensock < 0){LOG(FATAL, "socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, sockfd is: %d", _listensock);//填充套接字信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;// 接收任意地址int n = ::bind(_listensock, (struct sockaddr*)&local, sizeof(local));if(n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}LOG(DEBUG, "bind success, sockfd is: %d", _listensock);// tcp是面向连接的,所以通信之前,必须要先建立连接, 服务器是被连接的// tcpserver启动,未来首先是要等待客户端建立连接n = ::listen(_listensock, gbacklog);if(n < 0){LOG(FATAL, "listen error");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success, sockfd is: %d", _listensock);
}
注意这里我填充网络字段用了 INADDR_ANY ,表示我使用任意IP进行通信,所以将来服务器端在启动时就可以通过./tcpserver + port 的形式运行服务器端了。
✈️服务器循环控制
在Loop循环中,我们负责接收并处理数据,与udp不同,Tcp刚刚也说了是面向连接的,而我们收数据就需要使用到accept()这个接口了,至于这个接口最重要的不过是返回值,返回的也是文件描述符,不同的是,有几个客户端连接就会返回几个文件描述符。
用下面的例子来说明:
阿熊是一个餐馆的一个服务员,不同的是阿熊的嘴皮子还是比较突出的,所以阿熊就被委以重任,在店门口拉客,每过来一个客人,阿熊就会热情的去打招呼并介绍这里的饭店,如果有人正好饿了被阿熊的真挚所打动来到这个饭店,阿熊就会对里面喊:服务员来一位,这时候张三就出来负责这批人的饮食了。
而阿熊则不会进入到餐馆里面,还是在餐馆外拉客,每次拉倒客人就会通知后厨来一个服务员,给新来的客人进行点餐服务。
这个例子实际上就揭示了accept()与listensockfd的关系:
阿熊实际上就是listensockfd,用来负责网络中与本机发起连接的客户端,而这些所谓的服务员,实际上就是accept接口的返回值,服务器每链接一个客户端就会单独返回一个文件fd来进行服务。同时为了可以手动的控制服务器的运行,我们添加一个布尔位,用来判断是否可以支持运行:
void Loop()
{_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}}_isrunning = false;
}private:uint16_t _port;int _listensock;bool _isrunning;
✈️Service服务
监听成功获取链接,接着就可以设计服务了,跟udpServer一样,服务方式有很多种,单进程,多线程等方式进行服务实现,首先我们来实现Version 0版本,单进程服务,直接实现一个服务接口,负责对数据进行处。
当执行到Service这一步,表示当前已经链接成功,我们需要获取客户端的端口号和IP也就是说我们需要之前UdpServer实现的InetAddr类(用来记录IP和PORT),这样我们就有了客户端的信息,同时我们需要知道是哪个客户端通过哪个套接字传递的信息。综上所述Service接口的参数必须要有sockfd,与InetAddr类对象作为形参。
void Service(int sockfd, InetAddr client) {}// Service服务
首先我们可以通过客户端的sockfd来对消息进行接收,首先设置一个缓冲区,使用read()接口对客户端发来的信息放在缓冲区,当然read()的返回值表示读取的字节数,如果我们的缓冲区大小比较小是不能一次性读完的,所以我们要分批次读取数据,如果数据读取完毕,返回值为0,如果读取失败返回值为负数,所以需要将其放在循环内:
void Service(int sockfd, InetAddr client)
{LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";while(true){char inbuffer[1024];ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);if(n > 0){// n > 0 进行处理动作}else if(n == 0){//client 退出 && 关闭链接LOG(INFO, "%s quit", clientaddr.c_str());break;}else{LOG(ERROR, "read error", clientaddr.c_str());break;}}::close(sockfd);// 不关闭会导致 文件描述符泄漏的问题(文件描述符不归还)
}
当接收的返回值>0我们对数据进行处理,我们不做过多的处理,我们将数据在前面加上提示标语,然后再将数据原封不动的发回去,这就是我们的处理动作。所以完整的Service如下:
void Service(int sockfd, InetAddr client)
{LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";while(true){char inbuffer[1024];ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);if(n > 0){inbuffer[n] = 0;std::cout << clientaddr << inbuffer << std::endl;std::string echo_string = "[server echo]# ";echo_string += inbuffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){//client 退出 && 关闭链接LOG(INFO, "%s quit", clientaddr.c_str());break;}else{LOG(ERROR, "read error", clientaddr.c_str());break;}}::close(sockfd);// 不关闭会导致 文件描述符泄漏的问题(文件描述符不归还)
}
服务完成就必须要关闭文件描述符,不然就是导致文件描述符泄漏,我们在系统部分学习过,文件描述符也是有限的,如果我们每一次对客户端的sockfd不归还势必会导致资源的浪费,也就是 文件描述符泄漏问题。实际上,只要是有限的资源我们不归还都有可能会导致泄漏问题。
✈️Tcp客户端
在运行之前我们还需要对客户端代码进行编写,Tcp客户端与Udp客户端代码编写差别很小,首先进行参数控制,让其将来以 ./tcpclient + ip + port 的形式运行,接着调用socket()函数创建socket套接字。
创建完套接字之后,我们是需要通过套接字进行网络通信的,那么就需要绑定客户端IP和PORT,但是在UdpServer中我们说过,客户端是不需要绑定ip和port的,这是因为通常一个服务器不止一个ip可以访问,如果手动将其绑定就限定死只能通过这个ip来访问这个端口的服务。所以客户端是不需要绑定ip和port的,OS会自动帮你随机绑定。
虽然不用绑定端口号和IP,但是正常的网络通信我们需要进行,所以我们依旧需要填充sockaddr_in 字段,填充服务器端的信息以及IP类型:
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);// 表示进行tcp协议的通信if(sockfd < 0){std::cerr << "socket error" << std::endl;exit(2);}//tcp_client 要bind,但是不需要手动bindstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());return 0;
}
套接字创建完毕,网络字段也填充完毕,这个时候我们就可以通过 connect()
接口,通过sockfd,向对端发起连接,如果客户端首次调用connect接口,OS则会自动的将sockfd与对端sockaddr_in 对象进行绑定。
所以connect()内所传参数,需要服务器端的sockaddr_in 的对象信息,在上面,我们已将从命令行获取的端口号及IP,所以直接使用其IP和PORT对sockaddr_in 结构体进行填充即可。接下来我们就可以通过connect()将本地sockfd,与服务器端sockaddr_in 对象进行默认绑定了,并且朝着服务器端发起连接。
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;exit(2);}//tcp_client 要bind,但是不需要手动bindstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));if(n < 0){std::cerr << "connect error" << std::endl;exit(3);}return 0;
}
连接成功,客户端就可以向服务器端发起请求了,今天我们不做过多的处理,仅仅作为一个聊天室存在,从命令行获取信息,并且发送到服务器端,而在Tcp服务里面,我们更喜欢使用的发送消息的接口是send
,收消息使用recv
:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
与sendto和recvfrom不同的是recv和send不需要sockaddr结构体字段,也就是不需要对端的网络ip和port,这是因为调用recv和send之前已经与对端建立了链接,所以直接使用recv和send来进行通信即可。
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;exit(2);}//tcp_client 要bind,但是不需要手动bindstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));if(n < 0){std::cerr << "connect error" << std::endl;exit(3);}while(true){std::cout << "Please Enter# ";std::string outstring;std::getline(std::cin, outstring);ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0);// writeif(s > 0){char inbuffer[1024];ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if(m > 0){inbuffer[m] = 0; std::cout << inbuffer << std::endl;}else {break;}}else{break;}}return 0;
}
✈️Version 0:单进程
我们使用单进程服务,在Loop获取链接之后,我们直接调用Service服务:
void Loop()
{_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// version 0: 单进程Service(sockfd, InetAddr(peer));}_isrunning = false;
}
✈️Version 1: 多进程服务
我们实现的是网络版聊天室,聊天室里总不能只有一个客户端进行通信,所以我们至少需要两个客户端进行通信,所以我们可以引入多进程。
创建进程之后,由于子进程会继承父进程的文件描述符表,这并不是共享,我们在系统部分也学过进程,我们知道子进程会继承父进程的文件描述符表,但是是以拷贝的形式继承,所以子进程与父进程都会看到 listensockfd,而我们的目的是让子进程单独处理请求,父进程负责通过accept()来获取客户端发来的请求。
也就是说,对于父进程来说accept()所返回的sockfd对自己不重要,所以父进程关闭自己文件描述符表的 sockfd,相反,accept获取请求是通过listensockfd,对于子进程来说也不重要,子进程可以关闭自己文件描述符表中的 listensockfd:
void Loop(){_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// Version 1: 采用多进程pid_t id = fork();if(id == 0){// child : 关心sockfd, 不关心listensock::close(_listensock);exit(0);}// father : 关心listensock, 不关心sockfd::close(sockfd);}_isrunning = false;}
我们通过多进程的目的是父进程可以不断的获取新链接,子进程不断地给新链接提供服务,但是别忘记了,父进程是需要回收子进程的,所以父进程无论以哪种方式回收子进程,都需要等待子进程结束才会继续执行新的循环。所以这样一来程序执行的顺序依旧是串行执行,这跟单进程又有什么区别呢?
没错,如果仅是如此,跟单进程的方式没有任何区别,但是我们可以在子进程中在进行fork(),创建出孙子进程,而执行fork()的子进程直接退出,这样父进程就可以接收到子进程退出信息,可以开始新的循环了。而孙子进程由于没有了父进程,则会被OS托孤,父进程变为了OS,这样我们让孙子进程执行任务,执行完直接就会被系统给回收了:
void Loop(){_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// Version 1: 采用多进程pid_t id = fork();if(id == 0){// child : 关心sockfd, 不关心listensock::close(_listensock);if(fork() > 0) exit(0);// 当前进程退出,父进程wait成功,执行新的循环// 孙子进程Service(sockfd, InetAddr(peer));// 孙子进程 变成孤儿进程 由系统接管exit(0);}// father : 关心listensock, 不关心sockfd::close(sockfd);waitpid(id, nullptr, 0);}_isrunning = false;}
至于父子进程为什么获取的文件描述符都是4,很简单,我们知道给用户分配的文件描述符都是从下标3开始的,因为0,1,2已经被占据,而我们定义的listensocfd就是 文件描述符3,而通过accept获取的sockfd就是文件描述符下标4,这个文件描述符表会被拷贝到孙子进程,孙子进程执行结束直接被回收了。
回到父进程时我们关闭了 accept返回的sockfd,这样以来下次父进程再次获取连接所分配的文件描述符还是4,复制到孙子进程还是四,不断循环,这样就能让文件描述符循环利用了。但是如果一次性连接多个进程就有可能分配多个文件描述符。
✈️Version 2: 多线程服务
线程可以真正共享进程的文件描述符表,所以并不需要像多进程那样将文件描述符关闭,并且创建回收进程的成本是要比线程大的多的。创建的线程需要调用Service()接口去处理客户端请求,所以我们需要sockfd,以及客户端sockaddr 对象,于此我们将其封装为一个结构体去处理:
class ThreadData
{
public:ThreadData(int fd, InetAddr addr):sockfd(fd),clientaddr(addr){}
public:int sockfd;InetAddr clientaddr;
};void Loop(){_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// versoin 2: 采用多线程pthread_t t;// to do...}_isrunning = false;}
每个线程都会执行自己的回调函数,我们将回调函数命名为HandlerSock,通过HandlerSock来调用Service来处理客户端请求:
// 线程回调函数 类内实现
void* HandlerSock(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);// to dodelete td;return nullptr;
}
因为回调函数是在类内实现的,而线程回调函数严格要求参数只有一个并且为 void*类型,但是类内函数第一个参数都会隐藏this 指针,所以我们可以将函数设置为静态成员函数,并且在Struct ThreadData结构体中添加 this指针字段,以便于进行线程IO:
class TcpServer;// 声明class ThreadData
{
public:ThreadData(int fd, InetAddr addr, TcpServer* s):sockfd(fd),clientaddr(addr),self(s){}
public:int sockfd;InetAddr clientaddr;TcpServer *self;
};static void* HandlerSock(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);td->self->Service(td->sockfd, td->clientaddr);delete td;return nullptr;
}void Loop()
{_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// versoin 2: 采用多线程pthread_t t;ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);pthread_create(&t, nullptr, HandlerSock, td);}_isrunning = false;
}
但是这样以来,似乎依旧不能实现并发,因为我们知道,main线程需要等待其他线程回收成功之后才会向下执行,而我们main线程是在循环内部的,也就是说其他线程没有回收就不会接收新的客户端。所以会导致我们执行起来依旧是串行执行。不过请不要忘记了,线程可以detach(),也就是线程分离,如果线程进行了分离,main thread就不会等待所有线程回收在向下执行了。
// 线程回调函数
static void* HandlerSock(void* args)
{pthread_detach(pthread_self());// 进行线程分离ThreadData* td = static_cast<ThreadData*>(args);td->self->Service(td->sockfd, td->clientaddr);delete td;return nullptr;
}
✈️Version 3: 线程池服务
不论是多进程还是多线程,它们获取连接之后才会创建进程或者线程,创建的过程本身就在消耗时间,这个时间在客户端方面也是可以感受的到的,所以我们可以采用池化技术,预先创建一批线程,等到需要的时候直接拿来用即可,我们将线程池的代码引入进来。
我们使用function来封装线程调用函数,将参数设置为空,返回值设置为void:
using task_t = std::function<void()>;// using 表示重命名与 typedef 作用相同
但是我们的Service服务的参数有两个,这个时候使用 task_t 类型 调用 Service接口肯定是会报错的,所以我们在调用前首先进行绑定,固定的将sockfd与sockaddr 对象传入,这样就可以调用了,最后线程池在获取单例执行任务即可:
using task_t = std::function<void()>;void Loop()
{_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);if(sockfd < 0){LOG(WARNNING, "accept error");continue;}// Version 3: 采用线程池池化技术task_t t = std::bind(&TcpServer::Service, this, sockfd, InetAddr(peer));ThreadPool<task_t>::GetInstance()->Enqueue(t);}_isrunning = false;
}