在博客『 Linux 』TCP套接字网络通信程序中实现了一个TCP的回响程序,依次使用了单进程,多进程,多线程以及线程池的方式进行实现;
单进程
对于单进程版本而言,本质上是一个单执行流的程序,而这个服务端因为需要调用Server
函数会使得一个服务端只为一个客户端提供服务,其他的客户端的服务必须等待前一个客户端断线,也就是服务端与客户端之间取消联系才能继续为其他的客户端提供服务,但依旧是只为一个客户端提供服务;
多进程
多进程的版本通过fork
创建子进程的方式使得一个服务端可以同时为多个客户端提供服务,但在实现过程中存在了一个问题,即子进程在退出时父进程必须进行等待,否则将会出现僵尸线程从而造成内存泄漏;
而对于这种情况提供了两种办法:
双重fork()
通过两次fork()
,第一次的fork()
创建子进程,第二次的fork()
在子进程中再次创建子进程,使得服务端通过双重fork()
创建出孙子进程,当孙子进程创建完毕后将子进程退出,此时由于子进程必须存在一个父进程来管理,所以这里的孙子进程将会被"托孤"给操作系统(通常情况下PID
为1
),当孙子进程退出时操作系统将无需观察或者等待子进程的状态从而提高服务端整体的效率;
忽略SIGCHLD
子进程变成僵尸进程的本质原因是,当子进程退出时会向父进程发送SIGCHLD
信号以告诉父进程自己已经执行完毕,当父进程接受到子进程的这个信号时将会把这个已经结束的子进程放置在自己的"处理队列"中,相当于形成一个约定,即会对子进程进行回收,但若父进程没有通过wait()
或者waitpid()
来回收子进程时子进程将一直成为僵尸进程,维持僵尸状态在父进程的"处理队列"中,直至父进程调用wait()
或waitpid()
进行回收;
但如果子进程发送给父进程SIGCHLD
信号时信号被父进程忽略,则这个子进程不会被父进程放到所谓的"处理队列"中,而是直接退出;
多进程版本的服务端解决了服务端只能为一个客户端提供服务的状态;
但多进程版本服务端的缺点很明显是,每当一个客户端向服务端发起连接并与服务端建立起连接时都表示当前进程又再次创建了一个进程,而进程本质上无论是创建线程的开销还是进程的维护代价都是很大的,同时当计算机中进程过多时将会加大操作系统的负载,同时当当前计算机中锁存在的进程要大于限制时操作系统将会根据优先级把一些没必要的进程杀死,这样反而"得不偿失";
多线程版本
创建一个进程的开销和代价是很大的,而创建一个线程的开销则小得多,创建进程需要为这个进程维护新的PCB
结构体,页表,虚拟地址空间,文件描述符等等,而创建一个线程只需要为这个线程维护一个新的TCB
结构体(在Linux中)以及开辟新的栈帧即可,因为在同一个进程中的多个线程将共享文件描述符表,进程地址空间等资源,因为线程是比进程更细的执行流;
但多线程版本同样存在一些短板,多进程版本的服务端由于服务端fork()
创建的进程,进程与进程之间是相互独立的,这包括文件描述符表,而在进行TCP网络通信时服务端必然会打开一个网络文件,并且打开这个网络文件时将会返回一个新的TCP套接字文件描述符,而这个套接字描述符是占用文件描述符表的;
意思是服务端每新创建一个相同的进程实际上在关闭了多余的文件描述符后每个子进程因处理客户端发来的请求的文件描述符是相同的,而多线程中由于同一个进程间的不同线程共享同一个文件描述符表,这表示在多线程版本中不存在多余的文件描述符,但同样的文件描述符是一个有限资源,当服务端中的文件描述符资源被使用完毕后,再次创建线程来进行网络通信时这次的网络通信将会失败;
同样的虽然创建线程更多代价是极小的,但苍蝇腿也是肉,每当来一个客户端的连接都需要一定的开销,就算平常情况可能不会有非常大的开销但必然存在峰值情况,即服务端可能会在一个时间点存在大量的客户端来向其发起连接;
同时服务端的服务分为长服务和短服务,如果只是单独的回响程序,在设计中应该考虑把这个服务设计为短服务,即每当一个客户端向服务端发起链接时服务端为这个客户端提供服务,当本次服务结束后应断开连接从而避免峰值,当客户端再次需要服务时需要重新向服务端发起连接;
线程池版本
线程池版本完美了解决了上述几个版本的问题;
服务为短服务,每当服务端接收到一个客户端的连接时将会与客户端建立连接并为这个客户端提供服务,当本次服务结束后则断开连接;
同时由于是短服务,每当服务端为一个客户端提供完服务时会断开连接,相应的会把TCP套接字描述符关闭,这也表示当多个客户端向服务端发起请求时,文件描述符在增多的同时必然也在随着本次服务结束被关闭而减少,相当程度的减少了峰值情况文件描述符被用尽的情况;
由于线程池会率先准备好一批线程,这也避免了服务端在每次与客户端建立起连接时才创建线程进行后续操作从而降低服务端整体的效率;
既然是一个英译汉的程序,那么必须存在对应的字典;
在当前目录下创建了一个名为dict.txt
文件,文件中存放了一些英译汉的简单词汇,英文和汉字之间以:
作为分隔符;
$ cat dict.txt
cat:猫
dog:狗
house:房子
car:汽车
tree:树
book:书
phone:电话
chair:椅子
table:桌子
key:钥匙
lamp:灯
window:窗口
shoe:鞋
hat:帽子
shirt:衬衫
door:门
pen:笔
clock:钟
river:河流
同时作为一个字典服务,那么必须对字典进行初始化,初始化字典即打开对应的字典文件,将文件中的内容一行行进行读取,并进行分割,将分割的左右部分以key-value
的结构放置在容器当中;
初始化
/* Init.hpp */
const std::string dictname = "./dict.txt";
class Init
{
public:
Init()
{
std::ifstream in(dictname.c_str());
if (!in.is_open())
{
lg(FATAL, "ifstream open fail");
exit(2);
}
std::string line;
while (std::getline(in, line))
{
std::string part1, part2;
Split(line, &part1, &part2);
dict.insert(part1, part2);
}
in.close();
}
private:
std::unordered_map<std::string, std::string> dict;
};
在这段代码中首先使用const string dictname
定义了C++文件流中需要打开的字典文件;
使用ifstream
并传入需要打开的字典文件路径作为参数构造一个文件流对象,当这个文件流对象被实例化后默认为打开,当然也可能不为打开状态,所以调用is_open()
成员函数判断当前文件流是否被打开,如果没有被打开则直接退出服务端;
当打开了这个文件后需要对字典文件中的内容进行按行读取,这里调用了getline
函数进行了按行读取,这里使用了while
循环,当getline
函数读取成功时将会返回对应读取到的字符串对象,读取失败则返回空;
并且调用了自定义函数Split
对按行读取的字符串进行分割,当分割完毕后将分割好的左右部分分别以key-value
的方式放置在哈希表unordered_map
容器中,当字典文件中的内容全部被读取完毕时将会调用;
字符串分割
字符串分割主要是调用string::find()
查找当前行是否存在分隔符:
,若是存在则调用string::substr()
将字符串进行分割为左右部分;
/* Init.hpp */
const std::string sep = ":";
Log lg;
static bool Split(const std::string &line, std::string *part1, std::string *part2)
{
auto pos = line.find(sep);
if (pos == std::string::npos)
{
return false;
}
*part1 = line.substr(0, pos);
*part2 = line.substr(pos + 1);
return true;
}
其中part1
和part2
为输出型参数,分割后的字串将会以对象的指针的方式传递给对应的part1
和part2
;
英汉互译
准确的来说既然是一个词典,那么文件与文件之间可以降低耦合度,那么字典的处理则可以依旧交给Init.hpp
文件中进行处理;
上述的英汉词汇在分割之后已经以key-value
的方式存放在了unordered_map
哈希表中了,那么只需要在哈希表中寻找对应的key
值并返回value
即可以完成一个简单的英汉互译功能;
/* Init.hpp */
class Init
{
public:
const std::string& translation (const std::string &key){
auto iter = dict.find(key);
if(iter==dict.end()){
return "Unknow";
}
return iter->second;
}
private:
std::unordered_map<std::string, std::string> dict;
};
任务类的设置
英汉互译的功能已经在Init
类中实现了,这个线程池版本的TCP程序主要依靠Task
任务类进行任务的实现,所以只要在任务类中调用字典功能服务端就可以使用字典功能;
/* Task.hpp */
class Task
{
public:
// 执行任务的主要函数
void run()
{
char buff[4096];
int n = read(sockfd_, buff, sizeof(buff));
if (n > 0)
{
buff[n] = 0;
std::cout << "Client key# " << buff << std::endl;
std::string ret = init.translation(buff);
write(sockfd_, ret.c_str(), ret.size()); // 发回客户端
}
else if (n == 0)
{
lg(INFO, "Client %s:%d quit... ", clientip_.c_str(), clientport_);
}
else
{
lg(WARNING, "read error , error message: %s", strerror(errno));
}
close(sockfd_);
}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};
这个任务类中run
函数构造了一个Init
类对象,并且接收了来自用户从键盘中输入的字符串,调用Init::translation()
函数并传入用户输入的英文单词从哈希表中匹配,最后将结果发回给客户端;
测试
class Task
{
public:
void run()
{
char buff[4096];
int n = read(sockfd_, buff, sizeof(buff));
if (n > 0)
{
buff[n] = 0;
std::cout << "Client key# " << buff << std::endl;
std::string ret = init.translation(buff);
write(sockfd_, ret.c_str(), ret.size()); // 发回客户端
}
else if (n == 0)
{
lg(INFO, "Client %s:%d quit... ", clientip_.c_str(), clientport_);
}
else
{
lg(WARNING, "read error , error message: %s", strerror(errno));
}
close(sockfd_);
}
};
这是当前服务端的读写操作,其中读操作进行了完善,当读操作失败时对应的会根据读取失败的情况来进行相应处理,当读到的字节数为0
时表示客户端已经退出,如果read
函数的返回值>0
则表示在调用read
时调用失败,将会打印出对应的错误码;
但这里的write
也会失败,失败的原因有两种:
错误的套接字描述符
当程序向一个错误的(不存在)的套接字描述符进行写入操作时写入操作将失败;
void run()
{
// ...
write(100, ret.c_str(), ret.size()); // 发回客户端
// ...
close(100);
}
假设将服务端相应回客户端的套接字描述符改为100
(这里的100
必然是不存在的文件描述符),对应的写操作将会失败,失败原因是在调用write
接口时使用了错误(不存在)的文件描述符;
原版的整体框架为服务端提供长服务,所以客户端只需要向服务端发起一次连接即可;
// 原版的客户端
/* tcpclient.hpp */
class TcpClient {
public:
TcpClient(const std::string& ip, int16_t port)
: sockfd_(-1), ip_(ip), port_(port) {}
void Init() {
// 创建TCP套接字
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0) {
printf("socket error , error message: %s\n", strerror(errno));
exit(2);
}
}
void Request() {
std::string request;
char buff[4096];
while (true) {
// 发起请求
std::cout << "Please Enter# ";
std::getline(std::cin, request);
write(sockfd_, request.c_str(), request.size());
// 接收响应
int n = read(sockfd_, buff, sizeof(buff));
if (n > 0) {
buff[n] = 0;
std::cout << buff << std::endl;
} else if (n == 0) {
std::cout << "Server exit...." << std::endl;
break;
} else {
printf("Client read error, error message: %s\n", strerror(errno));
break;
}
}
}
void Start() {
// 对服务端发起连接 请求 以及接收服务端的响应
// 发起连接
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
inet_pton(AF_INET, ip_.c_str(), &(server.sin_addr));
server.sin_port = htons(port_);
socklen_t len = sizeof(server);
if (connect(sockfd_, (sockaddr*)&server, len)) {
printf("connect error , error message: %s\n", strerror(errno));
exit(3);
}
Request();
}
~TcpClient() {
// 关闭无用的TCP套接字描述符
if (sockfd_ != -1) {
close(sockfd_);
sockfd_ = -1;
}
}
private:
int sockfd_; // 套接字描述符
std::string ip_; // 用户传递的IP地址
int16_t port_; // 用户传入的端口号
};
/* client.cc */
void Usage() { printf("\n\tUsage : ./client ip port[port>1024]\n\n"); }
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
exit(-1);
}
// 获取用户输入的IP与端口号
int16_t port = std::stoi(argv[2]);
std::string ip(argv[1]);
// 实例化客户端对象
TcpClient tc(ip, port);
tc.Init();
tc.Start();
return 0;
}
但使用了线程池对服务端进行修改,并限制服务端只提供短服务时客户端就需要设计为每次向服务端发起一次请求时都需要重新向服务端发起连接,这是一个循环操作;
服务端每次为客户端提供一次短服务后会关闭与客户端的连接,客户端需要保证在自己不退出或者用户不允许客户端退出的情况下重新向服务端发起连接,当连接建立成功后重新让用户再次输入内容并将这个内容作为一个请求发给服务端,此时客户端需要重新创建一个套接字,因为当服务端关闭与客户端的连接后上一次的客户端的套接字描述符将变得无效;
在这段代码中创建套接字的功能被封装在Init()
成员函数中,所以只需要循环调用Init()
函数与Start()
函数就能完美的将客户端修改为与服务端匹配的客户端;
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
exit(-1);
}
// 获取用户输入的IP与端口号
int16_t port = std::stoi(argv[2]);
std::string ip(argv[1]);
// 实例化客户端对象
TcpClient tc(ip, port);
while (true) // 如果失败则重新为客户端对象进行一次初始化
// 初始化包括重新为客户端创建套接字等等
{
tc.Init();
tc.Start();
}
return 0;
}
在这段代码中循环调用了TcpClient
客户端类的初始化函数Init()
和执行函数Start()
;
同时在原版的客户端中客户端Request()
请求函数中与服务端建立连接后为了匹配服务端的多次让用户输入循环发起请求的动作也就显得没有必要,因为新的客户端会在服务端保持在线的状态循环重新创建套接字,重新发起连接最后重新发起请求;
void Request()
{
std::string request;
char buff[4096];
// 发起请求
std::cout << "Please Enter# ";
std::getline(std::cin, request);
int n = write(sockfd_, request.c_str(), request.size());
if (n < 0)
{
std::cerr << "warning write err..." << std::endl;
return;
}
// 接收响应
n = read(sockfd_, buff, sizeof(buff));
if (n > 0)
{
buff[n] = 0;
std::cout << buff << std::endl;
}
else if (n == 0)
{
std::cout << "Server exit...." << std::endl;
}
else
{
printf("Client read error, error message: %s\n", strerror(errno));
}
}
在这段函数中删除了客户端Request()
函数中冗余的while
循环;
同时上文提到了write()
会调用失败,所以同样的在这里对write()
进行了差错处理,但是客户端中并没有对SIGPIPE
进行错误处理,因为作为一个客户端需要让用户知道错误的位置在哪里而不是不让用户感知错误的位置以及具体问题;
测试
当服务端断线或者网络不稳定的情况下作为一个客户端都会感知到,并且重新向服务端发起连接,这个操作是在客户端中进行的,因为服务端虽然会对一个完成服务的客户端关闭连接但是不会关闭整个服务端,所以当客户端感知到网络不稳定或者是断线的情况下客户端应该主动向服务端发起连接;
发起连接的操作是在客户端类中的Start()
函数中的connect()
函数,可以使用do{}while()
在不存在冗余操作的前提下能够使客户端重新向服务端发起连接;
class TcpClient
{
public:
void Start()
{
isrunning_ = true;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
inet_pton(AF_INET, ip_.c_str(), &(server.sin_addr));
server.sin_port = htons(port_);
socklen_t len = sizeof(server);
int reconnectcnt = 5; // 用户断线重连的次数
do // do while 确保第一次能够正常连接
{
if (connect(sockfd_, (sockaddr *)&server, len))
{
printf("connect error, reconnecting(%d)...\n",reconnectcnt);
isreconnect_ = true;
reconnectcnt--;
sleep(1);
}
else
{
isreconnect_ = false;
}
} while (isreconnect_ && reconnectcnt);
if (reconnectcnt == 0)
{
printf("user unline...\n");
isrunning_ = false;
return;
}
Request();
}
bool GetRunningStat()
{
return isrunning_;
}
private:
bool isreconnect_; // 判断是否需要重连
bool isrunning_; // 判断客户端是否正在运行
};
这里进行了一个断线重连的处理,在成员变量中添加了一个bool
类型的isreconnect_
变量(默认为false
),在函数中定义了一个变量reconnectcnt
控制客户端重连的次数,使用do{}while()
控制连接,如果连接失败isreconnect_
将会被修改为true
代表连接失败需要重连,当reconnectcnt>0
且isreconnect_==true
两种情况下会进行重连,当连接次数减为0
时代表重连失败;
同时还定义了一个成员变量isrunning_
来判断当前的客户端是否还需要运行(默认为false
,调用Start()
函数时将被修改为true
),不需要运行的情况为重连失败对应的isrunning
会被修改为false
,这个成员变量主要是交给外层调用TcpClient
类的main()
函数的,因为main()
函数循环调用客户端类的Init()
和Start()
函数,在成员函数中无法终止外层main()
函数的循环,因此需要专门的一个成员变量作为条件变量,同时为了保证封装性,这个变量不能被暴露在publiuc
作用域中,所以需要再定义一个函数GetRunningStat()
来返回这个变量的状态,当main()
函数在循环调用Init()
函数和Start()
函数时需要判断这个客户端的状态来判断它是否有必要进入循环来进行下一次服务;
int main(int argc, char *argv[])
{
//...
// 实例化客户端对象
TcpClient tc(ip, port);
while (true) // 如果失败则重新为客户端对象进行一次初始化
{
tc.Init();
tc.Start();
if (!tc.GetRunningStat())
{
return 1;
}
}
return 0;
}
同时这里的reconnectcnt
参数不需要在循环中重新初始化,因为这个值是伴随一次服务的,若是本次掉线之后再次掉线,即连续两次掉线,那么第二次掉线将会在客户端下一次重新创建TCP套接字时重新连接;
测试
可以在服务端中使用setsockopt()
函数设置允许立即重新绑定使用了静态端口的套接字;
NAME
getsockopt, setsockopt - get and set options on sockets
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
函数不作过多解释;
/* tcpserver.hpp */
class TcpServer
{
public:
// 构造函数初始化 TcpServer 类
TcpServer(uint16_t port = defaultport) : listen_sockfd_(-1), port_(port) {}
void Init()
{
// 创建套接字 绑定 监听
// 套接字创建
listen_sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd_ < 0)
{
lg(FATAL, "create socket error , error message: %s", strerror(errno));
exit(SOCKET_ERR);
}
lg(INFO, "create socket sucess, sockfd:%d", listen_sockfd_);
int opt = 1;
setsockopt(listen_sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启
// 绑定
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;
socklen_t len = sizeof(local);
if (bind(listen_sockfd_, (sockaddr *)&local, len) < 0)
{
lg(FATAL, "bind error , error message: %s", strerror(errno));
exit(BIND_ERR);
}
// 设置监听
if (listen(listen_sockfd_, backlog) < 0)
{
lg(FATAL, "listen error , error message: %s", strerror(errno));
exit(LISTEN_ERR);
}
}
};
在服务端的Init()
函数中调用setsockopt()
函数设置放置偶发性的服务器无法进行立即重启;
这个服务端的字典服务以及客户端的编写也接近完成,但是还有一点与真正的服务不同,真正的服务并不会因为shell
的关闭而退出,而该服务端程序将会因为bash
进程的退出而退出,换句话来说就是当前的服务端生命周期是随当前bash
进程的;
假设有一个程序,程序代码为:
int main()
{
while (true)
{
cout << "hello world" << endl;
sleep(1);
}
return 0;
}
这个程序为循环每隔一秒打印一次hello world
;
要将字典服务进行守护进程化只需要添加一个小插件即可;
首先作为一个守护进程不能随意的被信号暂停,同时不能因为进程接收到的异常信号而崩溃退出,所以需要调用signal()
函数将对应的信号设置为忽略;
/* daemon.hpp */
void Daemon()
{
// 1.忽略异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
}
这里忽略了三个信号,分别为SIGCHLD
,SIGPIPE
和SIGSTOP
;
其中SIGCHLD
为防止产生僵尸进程,SIGPIPE
为防止服务端再向客户端进行写入途中连接被关闭而异常崩溃,SIGSTOP
则是防止某些用户使用SIGSTOP
信号暂停守护进程;
将可能出现的异常信号设置为忽略后即需要将自身变成独立的会话,变成独立的会话只需要进行一次fork()
,当子进程被成功创建后立即将父进程退出,子进程再调用setsid()
函数即可;
/* daemon.hpp */
void Daemon()
{
// 2.将自己变成独立的会话
if (fork() > 0)
exit(0);
setsid();
}
同时守护进程可能是一个服务,既然是一个服务那么可能需要将一些设置或者文件写入到操作系统中,那么则需要修改守护进程的工作目录,一个进程在运行时默认为在当前目录下运行,但是可以调用chdir()
函数来修改该进程的工作目录;
/* daemon.hpp */
void Daemon(const std::string &cwd = "")
{
// 3.更改当前调用进程的工作目录
if (!cwd.empty())
{
chdir(cwd.c_str());
}
}
这里将Daemon()
函数设置为了接收一个std::string&
的对象引用,用来传递可能需要修改的路径字符串;
同时服务端中可能出现大量的debug
消息,这些debug
消息与日志消息是打印在标准输出中的,而如果将这些信息全部打印在标准输出中将会影响服务端命令行整体的观感;
在Linux中位于/dev/null
文件是一个类似于回收站(垃圾桶)的文件,标准输入无法从这个文件中读取内容,但可以利用标准输出将文件写入到这个文件内表示为不需要的输出内容,如debug
所产生的信息,对应的如果需要保存日志信息,可以在自定义日志类对象中修改日志信息的输出方式(如输出在文件当中);
/* daemon.hpp */
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 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);
}
}
当这个插件被设计好后只需要在服务端中的Start()
函数中调用该函数则可以将该服务设置成为守护进程;
class TcpServer
{
public:
void Start()
{
Daemon(); // 调用守护进程插件
ThreadPool<Task>::getInstance()->Start();
// 获取连接 处理客户端请求
lg(INFO, "TcpServer start sucess...");
while (true)
{
// 获取连接
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
socklen_t len = sizeof(local);
int sockfd = accept(listen_sockfd_, (struct sockaddr *)&local, &len);
if (sockfd < 0)
{
lg(WARNING, "accept error , error message: %s", strerror(errno));
continue;
}
uint16_t client_port = ntohs(local.sin_port);
char ipstr[32];
inet_ntop(AF_INET, &(local.sin_addr), ipstr, sizeof(ipstr));
lg(INFO, "accept sucess , sockfd :%d ,client ip :%s ,client port :%d",
sockfd, ipstr, client_port);
Task t(sockfd, ipstr, client_port);
ThreadPool<Task>::getInstance()->Push(t);
}
}
};
系统自带的守护进程化接口为daemon()
;
NAME
daemon - run in the background
SYNOPSIS
#include <unistd.h>
int daemon(int nochdir, int noclose);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
daemon(): _BSD_SOURCE || (_XOPEN_SOURCE && _XOPEN_SOURCE < 500)
RETURN VALUE
(This function forks, and if the fork(2) succeeds, the parent calls _exit(2), so that further errors are seen by the child only.) On success daemon() returns zero. If an error
occurs, daemon() returns -1 and sets errno to any of the errors specified for the fork(2) and setsid(2).
daemon - run in the background
表示在后台运行;
nochdir
这个参数表示进程是否需要更改工作目录,如果传入0
则表示将进程的工作目录修改为根目录,否则进程的工作目录为当前目录;
noclose
这个参数表示是否需要把标准输入,标准输出和标准错误重定向到/dev/null
文件中,如果传入0
则表示需要,否则标准输入,标准输出和标准错误将不会被重定向到/dev/null
文件中(不动);
可以根据情况选择自己定义一个特定的守护进程化模块或者使用系统自带的守护进程化接口;
TCP协议是一种面向连接的协议,在建立连接时采用的是三次握手的的方式,当连接断开时采用四次挥手的方式;
三次握手是为了确保双方建立可靠连接的一种协议过程,四次挥手则是用于安全关闭已连接的操作;
三次握手
第一次握手
客户端发送一个SYN
报文给服务器,并进入SYN_SENT
状态,该保温包含了客户端的初始序列号(ISN
);
第二次握手
服务器接收到SYN
报文,发送了一个SYN-ACK
报文作为回应,该报文包含服务器的初始序列号,同时对客户端的SYN
请求进行确认(ACK(x+1)
),此时服务器进入SYN_RCVD
状态;
第三次握手
客户端接收到SYN-ACK
报文后,发送一个确认报文ACK
给服务器,表示确认接收到了服务器响应,携带ACK(y+1)
;
客户端进入ESTABLISHED
状态,同时服务器收到ACK
后也进入ESTABLISHED
状态;
这样就完成了三次握手;
四次挥手
第一次挥手
主动关闭方(一般是客户端)发送一个FIN(finish)
报文给对方,并进入FIN_WAIT_1
状态;
第二次挥手
被动关闭方(一般是服务器)收到FIN
报文后,发送一个ACK
报文进行确认,并进入CLOSE_WAIT
状态;
此时主动关闭放进入FIN_WAIT_2
状态;
第三次挥手
被动关闭方准备好关闭连接时,发送一个FIN
报文给主动关闭放并进入LAST_ACK
状态;
第四次挥手
主动关闭放收到FIN
报文后,发送最后一个ACK
报文进行确认,然后进入TIME_WAIT
状态,这个状态会持续一段时间以确保接收方接收到最后的ACK
;
被动关闭方在收到这个ACK
后进入关闭(CLOSE
)状态;
全双工通信指的是通信双方在同一个时间点内既可以发送数据也可以接收数据,与半双工不同,半双工要求发送和接收过程不能同时进行;
TCP的全双工本质上是TCP协议提供了两个缓冲区,分别为发送缓冲区和接收缓冲区,其中发送缓冲区;
当TCP套接字被创建好后将会默认创建对应的缓冲区,这个缓冲区实际上是两块内存空间;
仓库地址(供参考)
[Gitee - 半介莽夫 / CSDN - Dio夹心小面包]