🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:マリンブルーの庭園—ずっと真夜中でいいのに。
0:34━━━━━━️💟──────── 3:34
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
socket在TCP中主要接口的介绍
listen
accept
connect
setsockopt
telnet工具
Telnet命令行交互
什么是TCP协议?
通过程序来理解TCP
单进程版服务器
客户端
多进程版服务器
多线程版服务器
线程池版服务器
一些杂谈
守护进程化
前置知识
什么是守护进程
setsid
写出一个符合上述服务端的Daemon
系统中提供的Daemon
socket在TCP中主要接口的介绍
listen
listen
函数是在网络编程中常用的一个函数,特别是在使用套接字(sockets)进行服务器端的编程时。这个函数的主要作用是使套接字进入监听状态,等待客户端的连接请求。
函数原型通常如下(在C语言中):
#include <sys/types.h> #include <sys/socket.h>int listen(int sockfd, int backlog);
参数说明:
sockfd
:这是你要监听的套接字的文件描述符。这个套接字通常是之前通过socket
函数创建的,并且已经绑定到了一个特定的地址和端口上(通过bind
函数)。backlog
:这是等待连接队列的最大长度。换句话说,这是可以等待处理的未完成连接请求的最大数量。当队列满时,新的连接请求可能会被拒绝。这个值通常设置为5或更大,但具体的值取决于你的应用需求和系统限制。返回值:
- 如果函数成功,则返回0。
- 如果函数失败,则返回-1,并设置全局变量
errno
以指示错误。
在使用
listen
函数之前,你通常需要按照以下步骤设置套接字:
- 调用
socket
函数创建一个新的套接字。- 调用
bind
函数将套接字绑定到一个特定的地址和端口。- 调用
listen
函数使套接字进入监听状态。然后,你可以使用
accept
函数来接受客户端的连接请求。当accept
函数被调用时,它会阻塞(除非设置了非阻塞模式),直到有一个客户端连接请求到达。一旦有请求到达,accept
函数就会返回一个新的套接字描述符,这个描述符可以用于与客户端进行通信。
需要注意的是,listen
函数只是使套接字进入监听状态,它并不会接受任何连接请求。实际的连接请求是由accept
函数处理的。
accept
accept
函数是Linux网络编程中用于服务器端的一个关键函数,它的主要作用是接受客户端的连接请求,并返回一个新的套接字用于和客户端通信。(这个套接字可以理解成什么呢?我们可以理解为文件系统中的fd,也就是文件标识符,我们后续就是根据这个来像文件一样使用write、read进行通信的!)
函数的原型通常如下:
#include <sys/types.h> #include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd
:这是用于监听客户端连接请求的套接字文件描述符。这个套接字应该是之前通过socket
函数创建的,并且通过bind
和listen
函数进行了绑定和监听设置。addr
:这是一个指向struct sockaddr
的指针,用于保存客户端的套接字地址结构。这个结构包含了客户端的IP地址和端口号等信息。如果调用者对此信息不感兴趣,可以将此参数设为NULL。addrlen
:这是一个指向socklen_t
变量的指针,用于传入addr
结构的大小,并在函数返回时填充实际接收到的地址结构的大小。返回值:
- 如果成功,
accept
函数会返回一个新的套接字文件描述符,这个描述符可以用来与客户端进行通信。- 如果失败,则返回-1,并设置全局变量
errno
以指示错误。
调用accept
函数是一个阻塞的过程。如果没有客户端连接请求到来,accept
函数会一直阻塞,直到有客户端发起连接请求并成功完成三次握手。一旦有客户端连接请求成功,accept
函数会返回一个新的套接字描述符,这个套接字描述符专用于与刚刚连接的客户端进行通信。同时,客户端的地址信息会被填充到addr
所指向的结构中。
在实际编程中,accept
函数通常与bind
、listen
等函数结合使用,以完成一个完整的服务器端程序。在服务器开始监听客户端连接之前,需要先调用bind
函数将套接字绑定到一个特定的地址和端口,然后调用listen
函数使套接字进入监听状态。当有客户端连接请求到来时,再调用accept
函数接受连接,并返回新的套接字用于通信。
需要注意的是,如果套接字被设置为非阻塞模式,并且没有未完成的连接请求队列,accept
函数会立即返回并设置errno
为EAGAIN
表示资源暂时不可用。为了处理这种情况,可以使用select
或poll
函数来检查套接字上是否有传入的连接请求。
connect
connect
函数是编程中用于建立连接或关联的常用函数之一。在网络编程中,connect
函数主要用于客户端与服务器之间的连接建立。它的基本作用是在两个组件(在这种情况下,通常是客户端和服务器)之间建立通信通道,以便在不同组件之间传递数据或实现功能。
函数的原型通常如下:
#include <sys/types.h>#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
:这是由socket
函数创建的套接字文件描述符,代表客户端的套接字。addr
:这是一个指向struct sockaddr
的指针,包含了要连接的服务器的地址信息,包括IP地址和端口号。addrlen
:这是一个整数,表示addr
参数指向的地址结构的大小,通常通过sizeof(struct sockaddr_in)
或类似表达式获得。返回值:
- 如果连接成功,
connect
函数返回0。- 如果连接失败,
connect
函数返回-1,并设置全局变量errno
以指示错误原因。
在客户端编程中,connect
函数通常用于主动发起与服务器的连接请求。一旦连接建立成功,客户端和服务器就可以通过各自的套接字进行数据传输和通信。
需要注意的是,connect
函数是阻塞的,意味着它会等待直到连接建立成功或发生错误。如果服务器不可达或拒绝连接,connect
函数将返回错误。
与connect
函数相对应的是服务器端的accept
函数,它用于接受客户端的连接请求并返回一个新的套接字描述符用于通信。
setsockopt
setsockopt
函数是一个系统调用,主要用于在套接字层面上设置一些参数,以调整套接字的行为。它允许程序员配置套接字的各种属性。
函数的基本原型如下:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数说明:
sockfd
:标识一个套接字的描述字,即指向一个打开的套接口。level
:选项定义的层次,支持以下值:
SOL_SOCKET
:基本套接口IPPROTO_IP
:IPv4套接口IPPROTO_IPV6
:IPv6套接口IPPROTO_TCP
:TCP套接口
optname
:需设置的选项名称。optval
:指向存放选项待设置的新值的缓冲区。这个参数是一个指向变量的指针,变量的类型可以是整形、套接口结构或其他结构类型,如linger{}
或timeval{}
。optlen
:optval
缓冲区长度,表示optval
的大小。返回值:
- 如果设置选项成功,
setsockopt
返回0。- 如果设置选项失败,
setsockopt
返回-1,并且会设置errno
来表示具体的错误原因。
在编程中,使用setsockopt
函数可以针对特定的套接字进行各种配置,例如设置套接字的发送和接收缓冲区大小、启用或禁用广播等。通过合理地设置这些选项,可以优化网络性能,提高应用程序的可靠性和效率。
需要注意的是,setsockopt
函数的具体用法可能因操作系统和编程环境的不同而有所差异。因此,在使用该函数时,建议查阅相关的文档和资料,以确保正确理解和使用该函数。
telnet工具
在Linux系统中,telnet
是一个用于远程登录到另一台计算机或设备的命令行工具。它使用TCP协议,并允许用户通过命令行界面与远程系统进行交互。以下是关于Linux下telnet
使用的详细解释:
安装telnet
首先,你需要确保你的Linux系统已经安装了telnet
客户端。你可以使用包管理器来安装它。例如,在基于Debian的系统(如Ubuntu)上,你可以使用以下命令安装:
sudo apt-get install telnet
在基于Red Hat的系统(如CentOS或Fedora)上,你可以使用:
sudo yum install telnet
或者,如果你使用的是dnf
:
sudo dnf install telnet
使用telnet连接到远程主机:
一旦安装完成,你可以使用以下命令格式连接到远程主机:
telnet 远程主机IP地址 端口号
通常,telnet使用TCP端口23,因此如果你没有指定端口号,它将尝试连接到该端口。例如:
telnet 192.168.1.100
这将尝试通过TCP端口23连接到IP地址为192.168.1.100的远程主机。
在连接成功后,通常会提示你使用^[来进入,进入后可以向对应的TCP程序发送字节流。想要退出的话可以使用quit或者^]。
Telnet命令行交互
一旦连接建立成功,你就可以在telnet命令行界面中输入命令并与远程系统进行交互。这通常涉及到登录到远程系统(如果需要的话),然后执行各种命令。通常使用
Telnet选项和命令:
telnet
命令本身也支持一些选项和命令,用于控制telnet会话的行为。你可以通过查看telnet的手册页来获取这些选项的完整列表:
man telnet
一些常用的telnet选项包括:
-l
或--login
:自动登录到远程系统。-e
或--escape
:设置转义字符,默认为“^]”。-K
或--keep-alive
:使用TCP keepalive。
注意事项:
- 安全性:telnet在传输数据时并不加密,因此它被认为是不安全的,特别是在公共网络上。SSH(安全外壳协议)是一个更安全的替代方案,它提供了加密的数据传输。
- 防火墙和网络配置:确保远程主机的防火墙和网络配置允许telnet连接。在某些情况下,你可能需要在远程主机上配置telnet服务以接受连接。
- 终端类型:telnet会话可能需要指定终端类型(例如vt100)。这可以通过telnet的
-t
选项或远程系统的配置来完成。
什么是TCP协议?
TCP协议,全称传输控制协议(Transmission Control Protocol),是一种面向连接的、可靠的、基于字节流的传输层通信协议。该协议由IETF的RFC 793定义,主要用于在主机间建立一个虚拟连接,以实现高可靠性的数据包交换。
TCP协议的工作原理主要可以分为以下三个步骤:
- 建立连接:通信双方首先要建立TCP连接。客户端发送一个连接请求(SYN包)到服务器,并等待服务器的确认(ACK包)。服务器收到客户端的连接请求后,发送确认和自己的连接请求(SYN/ACK包)给客户端。客户端再发送确认(ACK包),至此连接建立完成。
- 数据传输:一旦连接建立,通信双方可以开始传输数据。发送方将数据划分成小块(称为报文段),并添加头部和校验等信息,然后通过TCP协议将这些报文段发送给接收方。接收方收到报文段后,校验数据的完整性,并把它们重新组装成完整的数据流。
- 可靠传输:TCP通过各种机制来保证数据的可靠传输。它使用序列号对每个报文段进行标记,并确保接收方按照正确的顺序进行数据重组。如果发送方发现某个报文段丢失或未收到确认,它会重新发送该报文段。另外,TCP也采用滑动窗口的机制,允许发送方连续发送多个报文段,而不需要等待确认。
TCP协议在网络通信中扮演着重要角色,尤其在需要高可靠性的数据传输场景中。然而,也需要注意到TCP协议在某些情况下可能存在性能问题,例如在网络拥塞或高延迟环境中,TCP的流量控制机制可能导致传输效率降低。因此,在选择使用TCP协议时,需要根据具体的应用场景和需求进行权衡。
重点总结如下:传输层协议、有连接、可靠传、输面向字节流
通过程序来理解TCP
单进程版服务器
这只是一个简单的单进程TCP,按照TCP的规则,首先获取对应IPV4协议和TCP协议的socket,然后创建对应的sockaddr_in用于后续的bind操作,OS在bind了对应的端口和地址后,与UDP不同的是,TCP需要通过listen使套接字进入监听状态,再通过accept来接受连接请求,需要注意accept会返回链接的sockfd,后续我们就可以根据这个sockfd进行相应的write和read的操作了(就像文件一样)。
#pragma once#include <iostream>#include <string>#include <cstdlib>#include <cstring>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <pthread.h>#include <signal.h>#include <signal.h>#include "Log.hpp"using namespace std;const int defaultfd = -1;const int defaultport = 8888;const std::string defaultip = "0.0.0.0";const int backlog = 10; // 但是一般不要设置的太大enum{ UsageError = 1, SocketError, BindError, ListenError,};class TcpServer{public: TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip) : _listensock(defaultfd), _ip(ip), _port(port) { } void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg.LogMessage(Info, "create socket success, _listensock: %d /n", _listensock); int opt = 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说) struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); inet_aton(_ip.c_str(), &(local.sin_addr)); if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg.LogMessage(Info, "bind socket success, _listensock: %d /n", _listensock); // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if(listen(_listensock,backlog)<0) { lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg.LogMessage(Info, "listen socket success, _listensock: %d /n", _listensock); } void Start() { lg.LogMessage(Info, "tcpServer is running.... /n"); for(;;) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd=accept(_listensock,(struct sockaddr *)&client,&len); if (sockfd < 0) { lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; } uint16_t clientport=ntohs(client.sin_port); char clientip[32]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // version 1 -- 单进程版 Service(sockfd, clientip, clientport); close(sockfd); } } void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { // 测试代码 char buffer[4096]; while (true) { ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; std::cout << "client say# " << buffer << std::endl; std::string echo_string = "tcpserver echo# "; echo_string += buffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport); break; } } } ~TcpServer() {}private: int _listensock; uint16_t _port; string _ip;};
客户端
客户端同UDP一样不需要我们显示的bind,OS会自动帮我们bind,随机端口。那么是在什么时候进行绑定的呢?在客户端发起connect的时候,进行自动随机bind。客户端中的connect就是为了让服务端的accept响应建立联系的函数。后续我们就可以根据经过socket函数获得的sockfd进行相应的write和read的操作了(就像文件一样,这个socketfd注意是自己(客户端)使用socket函数获得的)。需要注意:这里我们做了重连的处理,也就是do while循环以及其中的内容。
#include <iostream>#include <cstring>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>void Usage(const std::string &proc){ std::cout << "/n/rUsage: " << proc << " serverip serverport/n" << std::endl;}// ./tcpclient serverip serverportint 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]); struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr)); while (true) { int cnt = 5; int isreconnect = false; int sockfd = 0; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { std::cerr << "socket error" << std::endl; return 1; } do { // tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口 // 客户端发起connect的时候,进行自动随机bind int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); if (n < 0) { isreconnect = true; cnt--; std::cerr << "connect error..., reconnect: " << cnt << std::endl; sleep(2); } else { break; } } while (cnt && isreconnect); if (cnt == 0) { std::cerr << "user offline..." << std::endl; break; } // while (true) // { std::string message; std::cout << "Please Enter# "; std::getline(std::cin, message); int n = write(sockfd, message.c_str(), message.size()); if (n < 0) { std::cerr << "write error..." << std::endl; // break; } char inbuffer[4096]; n = read(sockfd, inbuffer, sizeof(inbuffer)); if (n > 0) { inbuffer[n] = 0; std::cout << inbuffer << std::endl; } else{ // break; } // } close(sockfd); } return 0;}
多进程版服务器
只是改动了一些代码,加入了子进程的创建以及进程等待的少量代码。需要特别注意代码中的注释部分,该部分保证了是多进程而不是还为单进程。当然也可以不做上述的步骤,更改SIGCHLD信号:signal(SIGCHLD, SIG_IGN);让父进程不等待即可。
#pragma once#include <iostream>#include <string>#include <cstdlib>#include <cstring>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <pthread.h>#include <signal.h>#include <signal.h>#include "Log.hpp"using namespace std;const int defaultfd = -1;const int defaultport = 8888;const std::string defaultip = "0.0.0.0";const int backlog = 10; // 但是一般不要设置的太大enum{ UsageError = 1, SocketError, BindError, ListenError,};class TcpServer{public: TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip) : _listensock(defaultfd), _ip(ip), _port(port) { } void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg.LogMessage(Info, "create socket success, _listensock: %d /n", _listensock); int opt = 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说) struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); inet_aton(_ip.c_str(), &(local.sin_addr)); if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg.LogMessage(Info, "bind socket success, _listensock: %d /n", _listensock); // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if(listen(_listensock,backlog)<0) { lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg.LogMessage(Info, "listen socket success, _listensock: %d /n", _listensock); } void Start() { lg.LogMessage(Info, "tcpServer is running.... /n"); for(;;) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd=accept(_listensock,(struct sockaddr *)&client,&len); if (sockfd < 0) { lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; } uint16_t clientport=ntohs(client.sin_port); char clientip[32]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // version 2 -- 多进程版 pid_t id=fork(); if(id==0) { //child close(_listensock); if(fork() > 0) exit(0); //这一步做出后,子进程会马上退出被父进程wait到,父进程可以继续进行循环 Service(sockfd, clientip, clientport); //孙子进程, system 领养 close(sockfd); exit(0); } close(sockfd); // father pid_t rid = waitpid(id, nullptr, 0); (void)rid; } } void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { // 测试代码 char buffer[4096]; while (true) { ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; std::cout << "client say# " << buffer << std::endl; std::string echo_string = "tcpserver echo# "; echo_string += buffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport); break; } } } ~TcpServer() {}private: int _listensock; uint16_t _port; string _ip;};
多线程版服务器
要实现多线程版,我们要特别用到pthread_detach这个函数。他会将指定的线程标记为可分离的。当一个线程被标记为可分离时,该线程的资源(如堆栈和线程描述符)会在该线程退出时自动被系统回收,而不需要等待其他线程使用pthread_join函数来释放它们。接着对于线程传入的函数Routine我们要特别注意将this指针也传入,为什么呢?因为线程需要用static函数共享资源的缘故。因此需要额外构造一个ThreadData类来储存isockfd、clientip、clientport、this指针。
#pragma once#include <iostream>#include <string>#include <cstdlib>#include <cstring>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <pthread.h>#include <signal.h>#include <signal.h>#include "Log.hpp"using namespace std;const int defaultfd = -1;const int defaultport = 8888;const std::string defaultip = "0.0.0.0";const int backlog = 10; // 但是一般不要设置的太大enum{ UsageError = 1, SocketError, BindError, ListenError,};class TcpServer;class ThreadData{public: ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t) {}public: int sockfd; std::string clientip; uint16_t clientport; TcpServer *tsvr;};class TcpServer{public: TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip) : _listensock(defaultfd), _ip(ip), _port(port) { } void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg.LogMessage(Info, "create socket success, _listensock: %d /n", _listensock); int opt = 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说) struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); inet_aton(_ip.c_str(), &(local.sin_addr)); if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg.LogMessage(Info, "bind socket success, _listensock: %d /n", _listensock); // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if(listen(_listensock,backlog)<0) { lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg.LogMessage(Info, "listen socket success, _listensock: %d /n", _listensock); } static void *Routine(void *args) { pthread_detach(pthread_self()); ThreadData *td = static_cast<ThreadData *>(args); td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//??? delete td; return nullptr; } void Start() { lg.LogMessage(Info, "tcpServer is running.... /n"); for(;;) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd=accept(_listensock,(struct sockaddr *)&client,&len); if (sockfd < 0) { lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; } uint16_t clientport=ntohs(client.sin_port); char clientip[32]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // version 3 -- 多线程版本 ThreadData *td = new ThreadData(sockfd, clientip, clientport, this); pthread_t tid; pthread_create(&tid, nullptr, Routine, td); } } void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { // 测试代码 char buffer[4096]; while (true) { ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; std::cout << "client say# " << buffer << std::endl; std::string echo_string = "tcpserver echo# "; echo_string += buffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport); break; } } } ~TcpServer() {}private: int _listensock; uint16_t _port; string _ip;};
线程池版服务器
需要额外传入Task的任务,这个自己决定,对于线程池则是之前实现的单例模式的线程池,该线程池默认创建10个线程,通过Push这个接口传入任务,后续线程池会自动处理任务。
ThreadPool.hpp
#pragma once#include <iostream>#include <vector>#include <string>#include <queue>#include <pthread.h>#include <unistd.h>struct ThreadInfo{ pthread_t tid; std::string name;};static const int defalutnum = 10;template <class T>class ThreadPool{public: void Lock() { pthread_mutex_lock(&mutex_); } void Unlock() { pthread_mutex_unlock(&mutex_); } void Wakeup() { pthread_cond_signal(&cond_); } void ThreadSleep() { pthread_cond_wait(&cond_, &mutex_); } bool IsQueueEmpty() { return tasks_.empty(); } std::string GetThreadName(pthread_t tid) { for (const auto &ti : threads_) { if (ti.tid == tid) return ti.name; } return "None"; }public: static void *HandlerTask(void *args) { ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); std::string name = tp->GetThreadName(pthread_self()); while (true) { tp->Lock(); while (tp->IsQueueEmpty()) { tp->ThreadSleep(); } T t = tp->Pop(); tp->Unlock(); t(); } } void Start() { int num = threads_.size(); for (int i = 0; i < num; i++) { threads_[i].name = "thread-" + std::to_string(i + 1); pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); } } T Pop() { T t = tasks_.front(); tasks_.pop(); return t; } void Push(const T &t) { Lock(); tasks_.push(t); Wakeup(); Unlock(); } static ThreadPool<T> *GetInstance() { if (nullptr == tp_) // ??? { pthread_mutex_lock(&lock_); if (nullptr == tp_) { std::cout << "log: singleton create done first!" << std::endl; tp_ = new ThreadPool<T>(); } pthread_mutex_unlock(&lock_); } return tp_; }private: ThreadPool(int num = defalutnum) : threads_(num) { pthread_mutex_init(&mutex_, nullptr); pthread_cond_init(&cond_, nullptr); } ~ThreadPool() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } ThreadPool(const ThreadPool<T> &) = delete; const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=cprivate: std::vector<ThreadInfo> threads_; std::queue<T> tasks_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool<T> *tp_; static pthread_mutex_t lock_;};template <class T>ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
TcpServer.hpp
#pragma once#include <iostream>#include <string>#include <cstdlib>#include <cstring>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <pthread.h>#include <signal.h>#include <signal.h>#include "Log.hpp"#include "ThreadPool.hpp"#include "Task.hpp"using namespace std;const int defaultfd = -1;const int defaultport = 8888;const std::string defaultip = "0.0.0.0";const int backlog = 10; // 但是一般不要设置的太大enum{ UsageError = 1, SocketError, BindError, ListenError,};class TcpServer;class ThreadData{public: ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t) {}public: int sockfd; std::string clientip; uint16_t clientport; TcpServer *tsvr;};class TcpServer{public: TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip) : _listensock(defaultfd), _ip(ip), _port(port) { } void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg.LogMessage(Info, "create socket success, _listensock: %d /n", _listensock); int opt = 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); inet_aton(_ip.c_str(), &(local.sin_addr)); if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg.LogMessage(Info, "bind socket success, _listensock: %d /n", _listensock); // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if(listen(_listensock,backlog)<0) { lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg.LogMessage(Info, "listen socket success, _listensock: %d /n", _listensock); } static void *Routine(void *args) { pthread_detach(pthread_self()); ThreadData *td = static_cast<ThreadData *>(args); td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//??? delete td; return nullptr; } void Start() { lg.LogMessage(Info, "tcpServer is running.... /n"); ThreadPool<Task>::GetInstance()->Start(); for(;;) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd=accept(_listensock,(struct sockaddr *)&client,&len); if (sockfd < 0) { lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; } uint16_t clientport=ntohs(client.sin_port); char clientip[32]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // version 4 --- 线程池版本 Task t(sockfd, clientip, clientport); ThreadPool<Task>::GetInstance()->Push(t); } } void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { // 测试代码 char buffer[4096]; while (true) { ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; std::cout << "client say# " << buffer << std::endl; std::string echo_string = "tcpserver echo# "; echo_string += buffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport); break; } } } ~TcpServer() {}private: int _listensock; uint16_t _port; string _ip;};
一些杂谈
127.0.0.1本地回环
TCP协议是面相字节流的:可以用read、write。读写网络就像读、写文件一般。
为什么TCP、UDP中对于端口以及IP等等需要设置为网络字节序,而写入读出的信息却不用?端口号和IP地址需要使用网络字节序是为了确保在不同计算机系统之间能够被正确识别,而实际传输的数据则依赖于应用层的处理,不强制要求转换为网络字节序。这是由网络协议的设计和网络通信的需求所决定的。
signal(SIGPIPE, SIG_IGN);常常用于socket在信息被write或者read是socketfd被提前关闭(这个就同进程中的管道一样,可能会导致进程被杀死)。SIG_IGN就是为了防止进程被直接杀死。
守护进程化
前置知识
我们通常会通过ps -axj命令查看所有的进程,如下先认识一下几个概念:
session的概念:session(会话)是用户与操作系统之间建立的一个交互环境。当用户通过命令行工具或图形界面登录到Linux系统后,系统会为该用户创建一个独立的会话。这个会话包含了用户当前的工作环境和正在运行的程序,为用户提供了一个独立且隔离的工作空间。可以理解为我们通过xshell打开的一个新会话,我们每打开一个新的会话就会多一个型的session,那么session id则表示在同一个会话里面的进程!
PGID的概念:终端进程组ID通常指的是与某个特定终端(或称为控制终端、tty)相关联的前台进程组的ID。在终端中,前台进程组是当前用户正在与之交互的进程组,而后台进程组则是在后台运行的进程组。当用户在一个终端中启动多个进程时,这些进程可能属于不同的进程组,但只有一个进程组会成为前台进程组。
TTY的概念:TTY是Teletype的缩写,它指的是由虚拟控制台、串口以及伪终端设备组成的终端设备。这些设备提供了用户与计算机进行交互的接口。通过TTY设备,用户可以输入命令并查看计算机的输出。
守护进程实际的特点:
- 守护进程基本上都是以超级用户启动( UID 为 0 )
- 没有控制终端( TTY 为 ?)
- 终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)
组长的概念:进程组组长的进程ID(PID)与进程组ID(PGID)是相同的。这个特性使得系统可以方便地识别和管理进程组组长。进程组组长负责管理组内的其他进程,包括创建新的进程组、将其他进程加入到自己的进程组中,以及对组内的进程进行协调和控制。而进程组内其他成员的PGID则为组长的PID。作为组长如果退出了,那么他的组员是会出错误的!
守护进程实际上就是脱离了session,变成自成进程组、自成会话的进程!他的本质实际上就是孤儿进程的原理,利用fork()创建一个子进程(为什么呢?如果要调用setsid则不能作为组长!),父进程退出,子进程调用setsid()创建一个新的会话并成为会话领导,子进程则会被OS所领养。
介绍一个文件夹:/dev/null
/dev/null
是 Unix 和类 Unix 系统(如 Linux)中的一个特殊设备文件,它被称为“空设备”或“空文件”。当你向/dev/null
写入数据时,数据会被丢弃,就像被送入了一个黑洞。而从/dev/null
读取数据则什么也得不到(立即返回 EOF,即文件结束标记)。
/dev/null
常被用于以下几个目的:
- 丢弃输出:当你运行一个命令或程序,并且不想看到其输出时,你可以将其重定向到
/dev/null
。例如,command > /dev/null
会将标准输出重定向到/dev/null
,从而丢弃所有输出。如果你还想丢弃错误输出,可以使用command > /dev/null 2>&1
。- 提供一个空的文件输入:当某个命令或程序需要一个文件输入,但你希望提供一个空的内容时,可以使用
/dev/null
。例如,command < /dev/null
会将标准输入设置为来自/dev/null
,这样命令就不会从任何地方读取到数据。
/dev/null
在 shell 脚本编程和系统管理中非常有用,因为它提供了一个简单的方法来丢弃不需要的输出或提供一个空的输入源。简单来说,
/dev/null
是一个数据“黑洞”,你可以向它写入任何你想要丢弃的数据,或者从它读取(但什么也得不到)。
什么是守护进程
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lpd等(这里的结尾字母d就是Daemon的意思)。
在创建守护进程时,一个典型的步骤是父进程先创建子进程,然后父进程退出,子进程继续执行。这样子进程就成为了一个守护进程。守护进程会在后台运行,不受前台用户的影响,即使终端被关闭,守护进程也会继续运行。
守护进程的特点主要包括:
- 在后台运行。
- 独立于控制终端(tty)。也就是说,在终端控制的用户无法直接控制它。
- 周期性地执行任务或者等待处理某些事件。
- 一般不会随着用户的退出而结束,而是会一直运行,直到系统关闭或者接收到特定的终止信号。
setsid
setsid
是在 Unix 和 Linux 环境中用于处理进程和会话的一个重要工具。它既可以作为系统调用在程序中使用,也可以作为命令行工具来执行。下面是对 setsid
的详细解释:
作为系统调用
当
setsid
作为系统调用时,它允许一个进程创建一个新的会话,并成为该会话的领导。这在创建守护进程时尤其有用,因为守护进程需要在后台运行,且不受前台用户或终端会话的控制。
- 调用条件:只有当前进程不是会话领导时,才能成功调用
setsid
。如果已经是会话领导,则调用会失败,并返回错误。- 效果:成功调用
setsid
后,当前进程会成为新会话的会话领导和进程组领导,且新会话没有控制终端。此外,新会话的 ID 和进程组 ID 将与调用进程的 PID 相同。- 用途:常用于创建守护进程。守护进程是在系统启动时开始运行,并在系统关闭时终止的进程。它们通常在后台运行,执行一些系统级任务,如监听网络请求、管理硬件资源等。
作为命令行工具
setsid
也可以作为命令行工具来使用,用于在后台启动一个进程,并将其与当前终端会话分离。
- 语法:
setsid [选项] 命令 [参数]
选项
:用于控制setsid
的行为,如-w
表示等待命令完成后再返回,-c
用于指定要在后台运行的命令等。命令
:要在后台运行的程序。参数
:传递给命令的参数。
- 用途:常用于在终端关闭或会话结束后,仍需要继续运行的进程。例如,你可能使用
setsid
来启动一个长时间运行的任务,如备份操作或网络服务器,以确保即使你关闭了终端,这些任务也会继续执行。
示例
在编程中,你可能会看到类似以下的代码片段,用于创建一个守护进程:
pid_t pid = fork(); // 创建一个子进程if (pid < 0) { // 处理 fork 失败的情况} else if (pid > 0) { // 父进程退出,子进程继续执行 exit(0);} else { // 子进程中调用 setsid,创建新的会话并成为会话领导 if (setsid() < 0) { // 处理 setsid 调用失败的情况 } // 继续执行守护进程的初始化操作和任务...}
在命令行中,你可能会使用类似以下的命令来在后台启动一个程序:
setsid myprogram arg1 arg2 &
这条命令会在后台启动 myprogram
,并将其与当前终端会话分离。即使你关闭了终端,myprogram
也会继续运行。
写出一个符合上述服务端的Daemon
#pragma once #include <iostream>#include <cstdlib>#include <unistd.h>#include <signal.h>#include <string>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>const std::string nullfile="/dev/null";void daemon(const std::string &cwd=""){ // 1. 忽略其他异常信号 signal(SIGCLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); signal(SIGSTOP, SIG_IGN); // 2. 将自己变成独立的会话 if(fork()>0) exit(0); setsid(); // 3. 更改当前调用进程的工作目录 if (!cwd.empty()) chdir(cwd.c_str()); // 4. 标准输入,标准输出,标准错误重定向至/dev/null int fd = open(nullfile.c_str(), O_RDWR); if(fd > 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); }}
系统中提供的Daemon
在 C 语言中,你可以使用 daemon
函数来将当前进程转换为守护进程。daemon
函数定义在 <unistd.h>
头文件中,其原型如下:
#include <unistd.h>int daemon(int nochdir, int noclose);
daemon
函数接受两个整数参数:
nochdir
:如果此参数非零,则daemon
不会将当前工作目录更改为根目录(/
)。noclose
:如果此参数非零,则daemon
不会关闭标准输入、标准输出和标准错误输出。
daemon
函数的返回值是一个整数:
- 如果成功,返回 0。
- 如果失败,返回 -1,并设置
errno
以指示错误。
下面是一个简单的示例,展示了如何使用 daemon
函数:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/stat.h>#include <sys/types.h>int main() { pid_t pid; // 创建守护进程 if (daemon(0, 0) == -1) { perror("daemon failed"); exit(EXIT_FAILURE); } // 由于守护进程已经关闭了标准输入、输出和错误输出, // 如果需要记录日志,可以重新打开或重定向到某个文件。 int fd = open("/tmp/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd == -1) { perror("open failed"); exit(EXIT_FAILURE); } // 将标准输出和标准错误输出重定向到日志文件 dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); // 守护进程的主循环 while (1) { sleep(10); // 休眠10秒 printf("Daemon is still alive.../n"); // 写入日志 fflush(stdout); // 刷新输出缓冲区 } close(fd); // 关闭文件描述符(实际上这个代码不会被执行到,因为有一个无限循环) return 0;}
在上面的示例中,我们首先调用
daemon(0, 0)
来创建守护进程。然后,我们打开一个日志文件,并将标准输出和标准错误输出重定向到该文件。最后,守护进程进入一个无限循环,每隔10秒向日志文件写入一条消息。请注意,由于守护进程在后台运行并且已经脱离了控制终端,因此它们不会响应终端信号(如 SIGINT)。如果你需要优雅地关闭守护进程,你应该实现一种机制来接收和处理适当的信号(如 SIGTERM)。
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~