Tcp协议Socket编程

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

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/475814.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式系统中QT实现网络通信方法

大家好,今天主要给大家分享一下,如何使用QT中的网络编程实现。 第一:QT网络编程基本简介 QT中网络模块为提供了可以使用TCP/IP客户端与服务器的类。它提供了较低级别的类,例如代表低级网络概念的 QTcpSocket, QTcpServer 和 QUdpSocket,以及诸如 QNetworkRequest, QNetw…

【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)

【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现&#xff08;Kalman Filter&#xff09; 更新以gitee为准&#xff1a; 文章目录 数据预测概念和适用方式线性系统的适用性 数据预测算法和卡尔曼滤波公式推导状态空间方程和观测器先验估计后验估计…

大模型时代的具身智能系列专题(十三)

迪士尼研究中心 瑞士苏黎世迪斯尼研究中心致力于不同领域的业务活动&#xff0c;其中包括电影、电视、公园和度假村以及消费产品。我们针对所有这些领域进行科研工作。我们开发能使我们将后道生产元素整合到前级生产中的技术。由此可节省许多昂贵的效果&#xff0c;这些效果最…

IDEA2023设置控制台日志输出到本地文件

1、Run->Edit Configurations 2、选择要输出日志的日志&#xff0c;右侧&#xff0c;IDEA2023的Logs在 Modify option 里 选中就会展示Logs栏。注意一定要先把这个日志文件创建出来&#xff0c;不然不会自动创建日志文件的 IDEA以前版本的Logs会直接展示出来 3、但是…

o1的风又吹到多模态,直接吹翻了GPT-4o-mini

开源LLaVA-o1&#xff1a;一个设计用于进行自主多阶段推理的新型VLM。与思维链提示不同&#xff0c;LLaVA-o1独立地参与到总结、视觉解释、逻辑推理和结论生成的顺序阶段。 LLaVA-o1超过了一些更大甚至是闭源模型的性能&#xff0c;例如Gemini-1.5-pro、GPT-4o-mini和Llama-3.…

AJAX的基本使用

AJAX的基本使用 &#x1f389;&#x1f389;&#x1f389;欢迎来到我的博客,我是一名自学了2年半前端的大一学生,熟悉的技术是JavaScript与Vue.目前正在往全栈方向前进, 如果我的博客给您带来了帮助欢迎您关注我,我将会持续不断的更新文章!!!&#x1f64f;&#x1f64f;&#x…

DDei在线设计器V1.2.43版发布

2024-11-21-----V1.2.43 一、bug 修复 1. 修复只读情况下&#xff0c;连线依然可以通过特殊点调整的 bug 2. 修复了同一页面多个实例时&#xff0c;部分方法只会引用最后一个实例的问题 3. 修复了组合控件和容器控件改变容器后没有清理的问题&#xff0c;优化了容器的实现 4. …

C++进阶:哈希表实现

目录 一:哈希表的概念 1.1直接定址法 1.2哈希冲突 1.3负载因子 1.4实现哈希函数的方法 1.4.1除法散列法/除留余数法 1.4.2乘法散列法 1.4.3全域散列法 1.5处理哈希冲突 1.5.1开放地址法 线性探测 二次探测 ​编辑 双重散列 1.5.2链地址法 二.代码实现 2.1开放地址…

鸿蒙NEXT开发案例:血型遗传计算

【引言】 血型遗传计算器是一个帮助用户根据父母的血型预测子女可能的血型的应用。通过选择父母的血型&#xff0c;应用程序能够快速计算出孩子可能拥有的血型以及不可能拥有的血型。这个过程不仅涉及到了简单的数据处理逻辑&#xff0c;还涉及到UI设计与交互体验的设计。 【…

(十八)JavaWeb后端开发案例——会话/yml/过滤器/拦截器

目录 1.业务逻辑实现 1.1 登录校验技术——会话 1.1.1Cookie 1.1.2session 1.1.3JWT令牌技术 2.参数配置化 3.yml格式配置文件 4.过滤器Filter 5.拦截器Interceptor 1.业务逻辑实现 Day10-02. 案例-部门管理-查询_哔哩哔哩_bilibili //Controller层/*** 新增部门*/Pos…

2024.5 AAAiGLaM:通过邻域分区和生成子图编码对领域知识图谱对齐的大型语言模型进行微调

GLaM: Fine-Tuning Large Language Models for Domain Knowledge Graph Alignment via Neighborhood Partitioning and Generative Subgraph Encoding 问题 如何将特定领域知识图谱直接整合进大语言模型&#xff08;LLM&#xff09;的表示中&#xff0c;以提高其在图数据上自…

amd显卡和nVidia显卡哪个好 amd和英伟达的区别介绍

AMD和英伟达是目前市场上最主要的两大显卡品牌&#xff0c;它们各有自己的特点和优势&#xff0c;也有不同的适用场景和用户群体。那么&#xff0c;AMD显卡和英伟达显卡到底哪个好&#xff1f;它们之间有什么区别&#xff1f;我们又该如何选择呢&#xff1f;本文将从以下几个方…

接口加密了怎么测?

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、定义加密需求 确定哪些数据需要进行加密。这可以是用户敏感信息、密码、身份验证令牌等。确定使用的加密算法&#xff0c;如对称加密&#xff08;如AES&am…

接口上传视频和oss直传视频到阿里云组件

接口视频上传 <template><div class"component-upload-video"><el-uploadclass"avatar-uploader":action"uploadImgUrl":on-progress"uploadVideoProcess":on-success"handleUploadSuccess":limit"lim…

springboot基于数据挖掘的广州招聘可视化分析系统

摘 要 基于数据挖掘的广州招聘可视化分析系统是一个创新的在线平台&#xff0c;旨在通过深入分析大数据来优化和改善广州地区的招聘流程。系统利用Java语言、MySQL数据库&#xff0c;结合目前流行的 B/S架构&#xff0c;将广州招聘可视化分析管理的各个方面都集中到数据库中&a…

VIM的下载使用与基本指令【入门级别操作】

VIM——超级文本编辑器 在当今时代&#xff0c;功能极其复杂的代码编辑器和集成开发环境&#xff08;IDE&#xff09;有很多。 但如果只想要一个超轻量级的代码编辑器&#xff0c;用于 Unix、C 或其他语言/系统&#xff0c;而不需要那些华而不实的功能&#xff0c;该怎么办呢&…

心情追忆-首页“毒“鸡汤AI自动化

之前&#xff0c;我独自一人开发了一个名为“心情追忆”的小程序&#xff0c;旨在帮助用户记录日常的心情变化及重要时刻。我从项目的构思、设计、前端&#xff08;小程序&#xff09;开发、后端搭建到最终部署。经过一个月的努力&#xff0c;通过群聊分享等方式&#xff0c;用…

开源代码统计工具cloc的简单使用

一.背景 公司之前开发了个小系统&#xff0c;要去申请著作权&#xff0c;需要填写代码数量。应该怎么统计呢&#xff1f;搜索了一下&#xff0c;还是用开源工具cloc吧&#xff01;我的操作系统是windows&#xff0c;代码主要是java项目和vue项目。 二.到哪里找 可以去官方下载…

基于单片机的条形码识别结算设计

本设计基于单片机的条形码辨识与结算系统。该系统主要用于超市、商场等场所的商品结算&#xff0c;实现了在超市内对不同种类商品进行自动识别及自动分类结算的功能。该系统由STM32F103C8T6单片机、摄像头、显示、蜂鸣器报警、按键和电源等多个模块构成。该系统可实现商品自动识…

进程间通信的信号艺术:机制、技术与实战应用深度剖析

目录 1 什么是信号 2 为什么要有信号 3 对于信号的反应 3.1 默认行为 3.2 signal()函数 -- 自定义行为对信号做出反应 3.3 对信号进行忽略 4 信号的产生的类型 4.1 kill命令 4.2 键盘输入产生信号 4.3 系统调用接口 4.3.1 kill() 4.3.2 raise() 函数 4.4 软件条件 …