学习目标
认识网络字节序等网络编程中的基本概念
学习socket api的基本用法
能够实现一个简单的udp客户端/服务器
能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本)
理解tcp服务器建立连接, 发送数据, 断开连接的流程
一、网络字节序
网络数据流有大端小端之分
网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
1.1 简述TCP/IP协议
TCP/IP协议规定网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
1. 2 网络字节序和主机字节序的转换
当我们进行网络编程时,需要将主机字节序的数据转换为网络字节序,反之亦然。为了解决这个问题,我们通常使用一些专门的函数进行字节序的转换。这些函数包括:
htonl
:将一个无符号长整形数从主机字节序转换为网络字节序。
ntohl
:将一个无符号长整形数从网络字节序转换为主机字节序。
htons
:将一个无符号短整形数从主机字节序转换为网络字节序。
ntohs
:将一个无符号短整形数从网络字节序转换为主机字节序。这些函数的名字中的 "h" 表示 "host","n" 表示 "network","s" 表示 "short","l" 表示 "long"。例如,
htonl
的全名是 "host to network long",意思是将一个长整形数从主机字节序转换为网络字节序。
二、socket api
(1) socket
这个函数用于创建一个新的套接字。它需要三个参数:协议族(例如,AF_INET 表示 IPv4,AF_INET6 表示 IPv6),套接字类型(例如,SOCK_STREAM 表示 TCP,SOCK_DGRAM 表示 UDP),以及协议(通常设置为0,表示采用默认协议)。如果成功,这个函数返回一个可以用于通信的套接字描述符
(2) bind
这个函数用于将套接字与特定的 IP 地址和端口号绑定。它需要三个参数:一个套接字描述符,一个指向
struct sockaddr
的指针(这个结构包含了你想要的 IP 地址和端口号),以及这个结构的大小。通常在服务器端使用。
1)struct sockaddr
struct sockaddr
是一个通用的套接字地址结构,用于保存网络地址信息。这个结构在<sys/socket.h>
头文件中定义。它被设计为一个通用的、可以处理任何类型的地址的结构,包括但不限于 IP 地址。
下面是这个结构在大多数系统中的定义:
struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address };
sa_family
字段表示地址的类型,例如AF_INET
表示 IPv4 地址,AF_INET6
表示 IPv6 地址。这个字段告诉系统如何解析sa_data
字段中的内容。
sa_data
字段包含了实际的地址信息,但是因为它是一个字符数组,所以不能直接用于访问 IP 地址或者端口号。因为
struct sockaddr
的通用性,一般我们在实际编程中不会直接使用它,而是使用特定协议的地址结构,例如struct sockaddr_in
(用于 IPv4),然后在需要struct sockaddr
作为参数的函数调用中进行类型转换。
(3) recvfrom
这个函数用于接收数据,通常用于 UDP 协议。它的参数包括一个套接字描述符,一个缓冲区用于存储接收的数据,缓冲区的大小,一些标志,一个指向
struct sockaddr
的指针用于存储发送者的地址,以及一个指向地址的长度的指针。
(4) sendto
这个函数用于发送数据,通常用于 UDP 协议。它的参数包括一个套接字描述符,一个包含要发送数据的缓冲区,缓冲区的大小,一些标志,一个指向
struct sockaddr
的指针包含了接收者的地址,以及这个地址的长度。
(5) listen
这个函数用于使套接字进入被动监听状态,等待连接请求,通常用于 TCP 协议。它需要两个参数:一个套接字描述符,以及一个整数表示在处理前可以排队等待的最大连接数。通常在服务器端使用。
(6) connect
这个函数用于发起到另一个套接字的连接,通常用于 TCP 协议。它需要三个参数:一个套接字描述符,一个指向
struct sockaddr
的指针(这个结构包含了你想要连接的目标 IP 地址和端口号),以及这个结构的大小。通常在客户端使用。
(7) accept
这个函数用于接受连接请求并返回一个新的套接字描述符,这个新的描述符对应的是与客户端之间的连接,通常用于 TCP 协议。它需要三个参数:一个套接字描述符,一个指向
struct sockaddr
的指针用于存储连接的客户端的地址,以及一个指向地址的长度的指针。通常在服务器端使用。
三、UDP协议
传输层协议
无连接
不可靠传输
面向数据报
3.1 模拟实现简单UDP
这个程序可以接收来自多个客户端的消息,并将每个消息转发给所有已知的客户端。每次有新的客户端发送消息时,它都会被添加到服务器的已知客户端列表中
3.1.1 日志信息
#pragma once #include <cstdio> #include <ctime> #include <cstdarg> #include <cassert> #include <cassert> #include <cstring> #include <cerrno> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"}; #define LOGFILE "server.log" class Log { public: Log():logFd(-1) {} void enable() { umask(0); logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666); assert(logFd != -1); dup2(logFd, 1); dup2(logFd, 2); } ~Log() { if(logFd != -1) { fsync(logFd); close(logFd); } } private: int logFd; }; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr : stdout; fprintf(out, "%s | %u | %s | %s\n", log_level[level], (unsigned int)time(nullptr), name == nullptr ? "unknow" : name, logInfo); fflush(out); // 将C缓冲区中的数据刷新到OS fsync(fileno(out)); // 将OS中的数据尽快刷盘 }
3.1.2 服务端
//用于显示如何使用该程序。用户应当至少提供端口号,可以选择性地提供 IP 地址。 static void Usage(const std::string proc) { std::cout << "Usage:\n\t" << proc << " port [ip]" << std::endl; } class udpServer { public: //构造函数用于初始化服务器的 IP 地址、端口号和套接字描述符。 udpServer(int port,std::string ip="") :_port((uint16_t)port) ,_ip(ip) ,_sockfd(-1){} ~udpServer(){} //创建一个 UDP 套接字,并绑定给定的 IP 和端口。 void init() { //1 创建套接字 //udp是采用IPv4,无连接方式通信 //IPv4:使用 32 位地址空间来标识互联网上的设备。由四个十进制数(每个数范围从 0 到 255)组成,例如 192.168.0.1 _sockfd=socket(AF_INET,SOCK_DGRAM,0); //如果失败返回-1 if(_sockfd==-1) { logMessage(FATAL,"%s:%d",strerror(errno),_sockfd); exit(-1); } logMessage(DEBUG,"socket create sucessed:%d",_sockfd); //2 网络信息填充及绑定 //2.1 添加网络信息(ip、port) //sockaddr_in网络通信 sockaddr_un本地通信 struct sockaddr_in local; bzero(&local,sizeof(local)); //地址族(Address Family),指示地址的类型,常见的值包括 AF_INET(IPv4)、AF_INET6(IPv6) local.sin_family=AF_INET; //从主机字节序转换为网络字节序 local.sin_port=htons(_port); //INADDR_ANY,表示绑定到任意可用的 IP 地址。这通常用于服务器程序,表示接受来自任意 IP 地址的连接。 //自动进行从主机字节序转换为网络字节序 local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str()); //2.2 绑定网络信息 //sockaddr_in--->sockaddr根据地址簇进行区分 if(bind(_sockfd,(const sockaddr *)&local,sizeof(local))==-1) { logMessage(FATAL,"bind:%s:%d",strerror(errno),_sockfd); exit(-2); } logMessage(DEBUG, "socket bind success: %d", _sockfd); } //这是服务器的主要循环,用于接收消息,记录发送消息的客户端,并将消息路由给所有已知的客户端。 void start() { //输入缓冲区 char bufferIn[1024]; //输出缓冲区 char bufferOut[1024]; //死循环 while(true) { //记录远程传输端的ip、port struct sockaddr_in peer; socklen_t len = sizeof(peer); bzero(&peer,sizeof(peer)); //接受信息 ssize_t s=recvfrom(_sockfd,bufferIn,sizeof(bufferIn)-1,0,(sockaddr*)&peer,&len); if(s>0) { bufferIn[s]='\0'; } uint16_t peerPort=ntohs(peer.sin_port); std::string peerIp=inet_ntoa(peer.sin_addr); //读取成功 logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, bufferIn); //检查在线成员,有就添加 checkOnlineUser(peerIp, peerPort, peer); //消息路由,给所有在线成员发送消息 messageRoute(peerIp, peerPort,bufferIn); } } private: //检查发送消息的客户端是否已知。如果不是,则将其添加到已知客户端列表中。 void checkOnlineUser(std::string peerIp, uint16_t peerPort,struct sockaddr_in peer) { std::string key=peerIp; key+=":"; key+=std::to_string(peerPort); //查询在线成员 auto iter =_users.find(key); if(iter == _users.end()) { _users.insert({key, peer}); } } //将收到的消息路由给所有已知的客户端。 void messageRoute(std::string ip, uint16_t port, std::string info) { std::string message = "["; message += ip; message += ":"; message += std::to_string(port); message += "]# "; message += info; for(auto &user : _users) { sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second)); } } private: //端口号 uint16_t _port; //ip地址 std::string _ip; //套接字 int _sockfd; std::unordered_map<std::string,sockaddr_in> _users; }; int main(int argc,char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); exit(3); } uint16_t port = atoi(argv[1]); std::string ip; if (argc == 3) { ip = argv[2]; } Log enable(); udpServer svr(port, ip); svr.init(); svr.start(); return 0; }
3.1.3客户端
static void Usage(const std::string proc) { std::cout << "Usage:\n\t" << proc << "serverIp serverPort" << std::endl; } void *recverAndPrint(void *args) { while (true) { int sockfd = *(int *)args; char buffer[1024]; struct sockaddr_in temp; socklen_t len = sizeof(temp); ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len); if (s > 0) { buffer[s] = 0; std::cout << "server echo# " << buffer << std::endl; } } } int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(3); } // 1 设置将要访问的服务器ip、port uint16_t serverPort = atoi(argv[2]); std::string serverIp = argv[1]; // 2 创建套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { logMessage(FATAL, "%s:%d", strerror(errno), sockfd); exit(-1); } logMessage(DEBUG, "socket create sucessed:%d", sockfd); // 3 添加网络信息(ip、port) struct sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); server.sin_addr.s_addr = inet_addr(serverIp.c_str()); //多线程用于收发消息 pthread_t t; pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd); // 4 通讯过程 std::string buffer; while (true) { std::cerr << "Please Enter# "; std::getline(std::cin, buffer); // 发送消息给server //客户端在sendto过程中,自动绑定网络信息,由操作系统维护 sendto(sockfd,buffer.c_str(),buffer.size(),0,(const sockaddr*)&server,sizeof(server)); } close(sockfd); return 0; }
3.2 常用的测试指令
3.2.1 netstat
netstat
命令用于显示网络连接、路由表、接口统计等信息。
netstat -lnup
命令的具体作用是显示与UDP相关的正在监听的套接字及其关联的进程。让我们解释这些选项:
-l
: 仅显示正在监听的套接字。
-n
: 显示数值地址,而不是解析主机名。
-u
: 显示UDP连接。
-p
: 显示每个套接字所属的进程ID和程序名称。注意本地环回ip:127.0.0.1 表示本主机通信
3.2.2 ps
ps axj:
a
: 显示所有进程(包括所有用户的进程)。
x
: 显示没有控制终端的进程。例如后台进程。
j
: 显示扩展格式的信息,包括进程组 ID (PGID)、会话 ID (SID)、控制终端 (TTY),以及进程与会话的关联状态。使用
axj
选项可以得到一个详细的进程列表,其中包含了与进程相关的会话和进程组信息。ps -aL:
-a
: 显示所有进程(包括其他用户的进程)。
-L
: 显示所有的线程,而不仅仅是进程。在现代操作系统中,一个进程可以包含多个执行线程。使用
-aL
选项可以查看所有进程的所有线程。
四、TCP协议
传输层协议
有连接
可靠传输
面向字节流
4.1 模拟实现简单TCP
这个程序可以接收来自多个客户端的任务,并将任务添加到线程池中处理。
4.1.1 任务
#include <iostream> #include <string> #include <functional> class Task { private: //std::function 是一个类模板,它为任何可调用对象(包括函数、lambda、函数对象和成员函数指针)提供了一个通用的类型。 using callback_t=std::function<void(int,uint16_t,std::string)>;//typedef std::function<void(int,uint16_t,std::string)> callback_t; public: Task():_sock(-1), _port(-1){} Task(int sock, uint16_t port, std::string ip, callback_t func) :_sock(sock), _port(port), _ip(ip),_func(func) {} ~Task(){} void operator () () { logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",pthread_self(), _ip.c_str(), _port); _func(_sock, _port, _ip); logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",pthread_self(), _ip.c_str(), _port); } private: int _sock; uint16_t _port; std::string _ip; callback_t _func; };
4.1.2服务端
#include "util.hpp" #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1),_tp(nullptr) {} ~ServerTcp() {} public: void init() { // 1 创建socket /* PF_INET和AF_INET相同 SOCK_STREAM是由于TCP面向字节流 注意此时返回的是监听套接字,并不提供服务 */ listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. 网络信息填充及绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 绑定服务器信息,从用户栈写入内核 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3 监听socket if (listen(listenSock_, 5) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 创建线程池 _tp=ThreadPool<Task>::getInstance(); } void loop() { // 运行线程池 _tp->start(); while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4 获取连接 /* 此时获得的文件描述符代表提供服务的套接字 如果获取链接时退出,那么服务器将无法重启 并且注意服务结束后要关闭这个文件描述符,否则会造成内存泄漏 */ int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); //创建任务 /* bind 用于绑定参数并生成一个新的可调用对象,可以使用占位符(std::_1, std::_2, ...)来表示参数,允许稍后在调用绑定的函数时提供这些参数 */ Task t(serviceSock,peerPort, peerIp, std::bind(&ServerTcp::transService,this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); //添加到任务队列 //注意线实际上线程池不适合死循环任务,而是短任务,否则会长时间占用线程 _tp->push(t); } } // 大小写转化服务 void transService(int sock, uint16_t clientPort,const std::string &clientIp) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { inbuffer[s] = '\0'; if(strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 大小写转化 for(int i = 0; i < s; i++) { if(isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { //写端退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } close(sock); // 关闭服务套接字,防止文件描述符泄漏 logMessage(DEBUG, "server close %d done", sock); } //执行命令并返回执行信息 void execCommand(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char command[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, command, sizeof(command) - 1); if (s > 0) { command[s] = '\0'; logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command); // 考虑安全 std::string safe = command; if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink"))) { break; } /* popen() 用于从一个进程中创建另一个进程并与其通信的库函数。它提供了一种方法来执行shell命令并读取或写入到该进程的标准输入/输出。该函数返回一个文件指针,可以用于读取子进程的输出或向其写入输入,并且在完成读/写操作后,使用 `pclose()` 来关闭文件指针。 FILE *popen(const char *command, const char *type); */ FILE *fp = popen(command, "w"); if(fp == nullptr) { logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno)); break; } dup2(sock, fp->_fileno); fflush(fp); pclose(fp); logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command); } else if (s == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } close(sock); pclose(fp); logMessage(DEBUG, "server close %d done", sock); } private: int listenSock_; uint16_t port_; std::string ip_; ThreadPool<Task>* _tp; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl; } int main(int argc, char *argv[]) { if(argc != 2 && argc != 3 ) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if(argc == 3) ip = argv[2]; Log enable(); daemonize(); ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; }
4.1.3 客户端
#include "util.hpp" volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } class clientTcp { public: clientTcp(uint16_t port, const std::string &ip) : _serverPort(port), _serverIp(ip), _sock(-1){} ~clientTcp(){} public: void init() { // 1 创建套接字 _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2 发起链接请求 // 2.1 填充远端服务器信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(_serverPort); inet_aton(_serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,并自动绑定 if (connect(_sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << _sock << std::endl; } void loop() { std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(_sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(_sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(_sock); } private: int _sock; uint16_t _serverPort; std::string _serverIp; }; int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); clientTcp client(serverPort,serverIp); client.init(); client.loop(); return 0; }
4.1.4 守护进程(精灵进程)
守护进程(精灵进程)是后台进程,独立于控制终端并周期性地执行某些任务或等待处理某些事件,不受登录登出的影响。通常用于服务器程序,如邮件服务器、系统日志服务器或任何其他需要在后台运行的程序。
(1)会话
会话是一个或多个进程组的集合且每个会话都有一个唯一的会话 ID。
会话开始于用户登录并持续到注销为止。通常每个终端窗口都是一个独立的会话。
会话的领头进程是创建该会话的进程。当你在终端中登录时,shell 通常是那个会话的领头进程
一个会话只能有一个领头进程。
当会话领头进程终止时,该会话中的所有进程都会收到 SIGHUP 信号。
在给定的会话中,任何时候都只能有一个进程组在前台运行。这意味着它可以从终端接收输入而其他的进程组在后台运行。
通常,会话的创建与控制终端的分离/获取有关。例如,守护进程通常会调用
setsid()
系统调用来开始一个新的会话并摆脱任何控制终端。
(2)创建守护进程(daemonize)
创建子进程并结束父进程: 这样可以确保进程不是进程组长,从而使得进程能成功调用setsid()。
在子进程中创建一个新会话: 使用setsid()创建新会话,使进程成为新会话的领头进程。这样可以确保进程不会再有控制终端。
改变当前工作目录: 通常会将当前工作目录更改为根目录,以确保进程不会阻止文件系统被卸载。
重设文件权限掩码: 这通常是为了确保进程可以读写其创建的任何文件。
关闭文件描述符: 进程从其父进程那里继承了文件描述符。不需要这些文件描述符,因此它们应该被关闭。
处理信号: 进程应该正确处理它可能收到的信号,忽略SIGPIPE。
void daemonize() { int fd = 0; // 1 忽略SIGHUP signal(SIGHUP, SIG_IGN); // 2 让自己不要成为进程组组长 if (fork() > 0) exit(0); // 3 设置自己是一个独立的会话 setsid(); // 4 更改进程的工作目录到根目录 chdir("/"); // 5 重设文件权限掩码 umask(0); // 6 重定向0,1,2,"/dev/null"为文件黑洞,读不返回,写丢弃 if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3 { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); // 7 关闭掉不需要的fd if(fd > STDERR_FILENO) close(fd); } }
(3)三种守护进程方案
1)传统模式
2)使用 daemon()
函数:
在某些系统中,有一个
daemon()
函数可以用来创建守护进程。基本上封装了上述的传统方法的大部分步骤。
3)使用 systemd
或其他现代初始化系统:
现代 Linux 系统通常使用
systemd
作为其初始化系统。在systemd
中,你可以创建一个服务单元文件来描述你的守护进程,并告诉systemd
如何启动和管理它。当systemd
启动服务时,它会处理创建守护进程所需的大部分步骤。使用这种方法,你可以避免手动编写守护进程的代码,并利用systemd
提供的许多其他功能,如日志记录、资源限制和依赖管理。
4.2 TCP协议通信流程
TCP(Transmission Control Protocol,传输控制协议)是一个面向连接、可靠的、基于字节流的通信协议。它确保数据在发送者和接收者之间正确无误地传输。下面是 TCP 通信的基本流程:
4.2.1连接建立(三次握手):
SYN: 客户端发送一个 SYN(synchronize)包给服务器,以请求建立连接。这个包中会包含一个客户端的初始序列号。
SYN-ACK: 服务器收到 SYN 包后,会确认请求并回应一个 SYN-ACK(synchronize-acknowledge)包。这个包中会包含服务器的初始序列号和客户端初始序列号的确认。
ACK: 客户端收到 SYN-ACK 包后,发送一个 ACK(acknowledge)包给服务器,确认它已收到服务器的初始序列号。此时,连接建立完成。
4.2.2 数据传输:
一旦连接建立,双方都可以开始发送数据。TCP 通过序列号和确认号确保数据的可靠传输。
如果数据包在网络中丢失或到达时出错,接收方不会确认那个数据包,发送方在超时后会重新发送那个数据包。
TCP 也提供流控制功能,这可以防止一方发送的数据速度超过另一方的接收能力。
4.2.3 连接终止(四次挥手):
FIN: 当一方完成数据发送后,它会发送一个 FIN(finish)包给另一方,请求关闭连接。
ACK: 另一方会确认这个 FIN 包。
FIN: 当另一方也完成数据发送后,它也会发送一个 FIN 包。
ACK: 第一方确认这个 FIN 包。此时,连接被完全关闭。
4.2.4错误处理和流控制:
TCP 提供了多种错误处理和流控制机制,如超时重传、滑动窗口、拥塞控制等,以确保数据的可靠传输。
整个 TCP 通信流程被设计得非常可靠,确保在不可靠的 IP 网络上提供稳定的、按顺序的、无差错的数据传输服务。