您的位置:首页 > 教育 > 锐评 > 一个人免费视频在线观看bd_优化大师win10_免费网站排名优化软件_关键词优化排名软件怎么样

一个人免费视频在线观看bd_优化大师win10_免费网站排名优化软件_关键词优化排名软件怎么样

2025/5/9 12:11:53 来源:https://blog.csdn.net/m0_57304511/article/details/146464051  浏览:    关键词:一个人免费视频在线观看bd_优化大师win10_免费网站排名优化软件_关键词优化排名软件怎么样
一个人免费视频在线观看bd_优化大师win10_免费网站排名优化软件_关键词优化排名软件怎么样

预备知识

理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。

源IP地址:发送请求的ip地址

目的IP地址:被请求的主机ip地址

查看IP地址:

ifconfig命令即可查看:inet后面的就是IP地址。

认识端口号

image-20221125173355336

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

注意:服务器中的端口号有自己的分配规则,有很多都有自己的确定的端口号,所以我们在指定端口号的时候建议不要取0~1023。

认识本地环回IP:127.0.0.1(代表本主机)

常用于在本地测试网络通信代码的时候使用,当发送的信息通过TCP/IP协议到达底层的时候,又会从底部重新回到上层(当前主机),而不是通过路由器到达另一台主机上。

理解 “端口号” 和 “进程ID”

问:为什么端口号不直接使用进程ID呢?

答:

  • 端口号使用进程ID的话,就是将进程管理和网络通信强耦合起来,不符合我们程序设计的原则,端口号单独使用的目的就是将进程管理和网络通信进行解耦(例如由于自身主机发生错误,那么网络间通信将会受到影响)
  • 端口号可以标识该进程是否进行网络通信:拥有端口号的进程说明该进程是要进行网络通信的,没有端口号的进程说明该进程不会也不需要进行网络通信

问:操作系统是如何通过端口号找到相对应的进程的?

答:操作系统内维护了两张哈希表,一张是key为进程的ID值,value为相应的进程的哈希表另一张是key为端口号,value为相应的进程的哈希表。同时根据这个原理:一个进程可以绑定多个端口号吗?是可以的,因为有可能只是进程中的一个子模块(线程)在进行通信。一个端口号可以绑定多个进程吗?不可以的,因为key->value是一对一的映射关系。

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”。

认识网络套接字

IP地址+端口号 = 网络套接字(socket)

{源IP:源端口号,目的IP:目的端口号} -> socket对

认识TCP协议

  • 传输层协议
  • 有连接(类似电话)
  • 可靠传输(必须成功发给对方,如果丢包或者信息出错还要重新发送,而且要等对方应答了才能丢掉发送的包等等)
  • 面向字节流

认识UDP协议

  • 传输层协议
  • 无连接(类似邮箱)
  • 不可靠传输(发给对方就算成功了,不管是否丢包,信息是否重复错误)
  • 面向数据报

网络字节序

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出; (先发的是低地址的数据,后发的是高地址的数据)
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可.

image-20221128130557984

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。

image-20221128130726189

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket编程接口

socket编程需要包含的头文件

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//前两个头文件包含的是套接字接口
//后两个头文件包含的是各种数据类型和操作各种数据类型的接口,比如主机转网络,网络转主机

socket 常见API

socket()

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
//参数介绍
domain:套接字的域,进行本地通信还是网络通信网络通信:AF_INET(当然,写成PF_INET也是可以的,两者并没有区别,表达的意思是完全一样的)AF_INET:IPV4AF_INET6:IPV6本地通信:AF_UNIX
type:套接字类型。决定了我们通信的时候对应的报文类型。流式:SOCK_STREAM(TCP通信)用户数据报式:SOCK_DGRAM(UDP通信)
protocol:协议类型。网络应用中我们一般将其设置为0,在本d通信中我们会将其设置为其它值。
返回值:如果创建成功,一个文件描述符会被返回,否则就返回-1

问:IPV4和IPV6有什么区别?

答:IPV6的出现主要是为了解决IP地址不够的现象,IPv4中规定IP地址长度为32,即有232-1个地址;而IPv6中IP地址的长度为128,即有2128-1个地址。

bind()

bind()函数用于服务器端,服务器的网络地址和端口号通常固定不变,客户端得知服务器的地址和端口号以后,可以主动向服务器请求连接。因此服务器需要调用bind()绑定地址。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
//参数介绍
socket:调用socket()函数返回的文件描述符。也就是套接字。
address:address指针所指向的空间里面存储的而是IP地址和端口号,我们使用的是struct sockaddr_in(网络通信)和struct sockaddr_un(本地通信)注意:使用struct sockaddr_instruct sockaddr_un需要包含<netinet/in.h>头文件和<arpa/inet.h>
返回值:绑定成功返回0,失败返回-1

字符串形式的ip和in_addr的互相转换

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);//点分十进制字符串转in_addr
//下面两个最常用,用于字符串和in_addr之间的互相转换
in_addr_t inet_addr(const char *cp);//字符串形式的IP转in_addr_t
//最常用且最简单的,将字符串类型的IP地址转换为4字节的整数,除了做转化之外,还会自动给我们进行主机转网络htonl
char *inet_ntoa(struct in_addr in);//in_addr转字符串形式的IPint inet_pton(int family, const char *src, void * addrptr);
//参数介绍
family:address family(协议族),支持的协议族有后面2种:AF_INET Inetnet的Ipv4协议  AF_INET Inetnet的Ipv6协议
src:是个指针,指向保存IP地址字符串形式的字符串。
addrptr:指向存放网络地址的结构体的首地址
返回值:成功返回1,如果src并没有指向一个IP地址的字符串,返回0,否则返回-1
const char *inet ntop(int family, const void *addrptr, char *strptr, size_t len);
//参数介绍
family:address family(协议族),支持的协议族有后面2种:AF_INET Inetnet的Ipv4协议  AF_INET Inetnet的Ipv6协议
addrptr:指向要转换为字符串的网络字节中的IP地址的指针。
strptr:指向缓冲区的指针,用于存储 IP 地址的 NULL 终止字符串表示形式。
返回值:失败返回NULL,成功返回一个指向缓冲区的指针即strptr
//其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

问:inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢?这个char*所指向的空间是哪个区域呢?

答:从手册中我们可以找到答案:image-20221129134346619

从上面的手册中我们可以看到,char*所指向的空间是在静态区上,不需要我们手动进行释放,当进程退出的时候会自动进行释放。

注意:因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

通过上面的分析,iner_ntoa这个函数可能是一个非线程安全的函数,所以在多线程的环境下,我们更推荐使用inet

recvfrom()

#include <sys/types.h>
#include <sys/socket.h>
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);
//参数介绍
sockfd:从特定套接字中进行读取
buf:	接收到的数据将存放到这个位置
len:buf参数指向的缓冲区的长度(以字节为单位)。
flags:0标识阻塞式读取
src_addr:输出型参数,发送方的socket信息
addrlen:输入输出型参数,发送方socket长度,当socket中被写入了发送方的信息时,len中此时就会被修正为发送方socket的实际大小
返回值:大于0说明已经读取到了,失败则返回-1
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

sendto()

#include <sys/types.h>
#include <sys/socket.h>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);
//参数说明
sockfd:当前套接字
buf:要发送的数据所在的位置
len:要发送的数据的长度
flags:0就是阻塞式发送
dest_addr:里面有接收方的ip地址和端口号
addrlen:sizeof(dest_addr)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

listen()

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
//参数介绍
socket:sock()函数返回的文件描述符
backlog:底层的全连接队列的长度,算法就是:n + 1,表示在不accept的情况下,最多能维护n+1个连接(全连接的状态),此时服务端维护已经建立连接的结构体会形成一个全连接队列,当该队列的长度到达n + 1后,新的连接无法正常建立连接,如果有客户端试图再建立连接服务端的该连接会处在SYN_RCVD(半连接的状态),客户端会处在ESTABLISHED,等刚才的某个连接断开之后,服务端才会进入ESTABLISHED

问:为什么要维护一个全连接队列?

答:此处运用了池化的技术,因为当服务端的某个线程正在处理的任务已经结束闲置下来之后,会从该队列中取队首元素,然后accept然后来进行处理。

问:为什么这个队列不能太长?

答:1、服务端为了维护这个队列,会占用系统资源,如果过于长会导致服务器效率低下,与其浪费这些系统资源,不如将这些资源通过多线程的方式用来给用户提供服务。2、服务端队列太长,会严重影响客户体验。

accept()

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
//参数介绍
socket:自己创建的套接字,用来获取新链接,我们也将其称为监听套接字
address/len:帮我我们获取连接的客户端的相关信息
返回值:成功返回一个socket套接字(主要是用来w),即一个文件描述符,失败返回-1

connect()

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
// 参数介绍
sockfd:创建的套接字fd。
addr:输入型参数,等价于sendto中的参数。
addrlen:输入输出型参数,等价于sendto中的参数。
返回值:成功返回0,失败返回-1//注意:当客户端调用connect时,系统会自动帮我们bind
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

image-20221128133532468

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址. sockaddr_un是域间通信,也就是本地通信
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段(前16位地址数据)确定结构体中的内容,如果操作系统发现对比之后发现传的参数的前16位是AF_INT,就将传的struct sockaddr*类型强转为struct sockaddr_in*类型,如果比对之后发现是AF_UNIX类型,就将传的struct sockaddr*类型强转为struct sockaddr_in*类型。
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

问:为什么上面函数中的struct sockaddr*类型为什么不设计成为void*类型呢?

答:在设计这些接口的时候并不存在void*类型并且当时的主流编译器也不支持void*类型。当然,站在后来者的视角来看,设计成为void*类型将会是一个不错的选择,但是换句话说了,使用C++的知识设计成为多态岂不是更妙哉?

sockaddr结构

struct sockaddr {sa_family_t sa_family; 		/* address family, AF_xxx */char sa_data[14];			/* 14 bytes of protocol address */
};

sockaddr_in 结构

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{__SOCKADDR_COMMON (sin_);//就是前16位,标识是AF_INET还是AF_UNIX//上面实际定义的变量如下所示:/sa_family_t sa_prefix##family; /in_port_t sin_port;			/* Port number.  */struct in_addr sin_addr;		/* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];
};

in_addr结构

/*Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};

代码练习

udp练习

代码功能:

服务器udpServer进程和客户端udpClient进程建立连接,客户端进程可以向服务端udpServer进程发送消息,客户端进程可以同时存在多个,客户端向服务端发送的进程将广播发送给所有的客户端进程。在客户端进程中,存在两个线程,一个是主线程,一个是新线程,主线程负责发送消息,新线程负责接收并输出消息。

udpServer.cpp文件

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <unordered_map>#include "Log.hpp"
using namespace std;
static void Usage(const string proc)
{ std::cout << "Usage\n\t" << proc << " port [ip]" << std::endl;
}
class UdpServer
{
public:UdpServer(int port, std::string ip = ""): _sockFd(-1),_port((uint16_t)port),_ip(ip){}~UdpServer(){}public:void init(){//1. 创建socket套接字_sockFd = socket(AF_INET, SOCK_DGRAM, 0);//就是打开了一个文件if (_sockFd < 0){logMessage(FATAL, "socket:%s:%d", strerror(errno), _sockFd);exit(1);}logMessage(DEBUG, "socket create success:%d", _sockFd);//2. 绑定网络信息:id+portstruct sockaddr_in  local;//local在哪里开辟的空间?用户栈 -> 临时变量 ->写入内核中//将指定空间的指定的字节都置为0,当然,也开始使用memsetbzero(&local, sizeof(sockaddr_in));//填充协议家族,域(本地or网络)local.sin_family = AF_INET;//填充服务器对应的端口号:最终是要发通过网络发送给对方的(必须本地转网络)local.sin_port = htons(_port); //填充服务器对应的ip地址,"aaa.bbb.ddd",字符串风格的点分十进制 -> 4字节IP地址 -> uint32_t ip// ip是采用位段的方式来进行存储的,比如下面这样// struct ip// {//     uint32_t part1 : 8;//     uint32_t part2 : 8;//     uint32_t part3 : 8;//     uint32_t part4 : 8;// }//INADDR_ANY(0):程序员不关心会bind到哪一个ip,这个参数就是绑定服务器上的所有IP,强烈推荐这种写法//inet_addr:指定填充确定的IP,特殊用途或者测试的时候使用//注意:云服务器进制bind云服务器上的任何确定IP,只能使用INADDR_ANY,虚拟机则不受这个限制local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());//注意:由于INADDR_ANY是0,所以没有必要对其进行主机到网络的转化//2.2 网络信息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);//对于udp服务器此时就已经成功了}void start(){//服务器设计的时候都是死循环char inbuffer[1024];//将来读取到的数据都放在这个缓冲区中char outbuffer[1024];//将来要发送的数据放在这个缓冲区中while(true){struct sockaddr_in peer;//输出型参数socklen_t len = sizeof(peer);//输入输出型参数// logMessage(NOTICE, "sever 提供 service中");// sleep(1);ssize_t s = recvfrom(_sockFd, inbuffer, sizeof(inbuffer) - 1, 0, \(struct sockaddr*)&peer, &len);if(s > 0){inbuffer[s] = 0;//当作字符串来进行看待}else if(s == -1){logMessage(WARNING, "recvfrom:%s:%d", strerror(errno), _sockFd);continue;}//读取成功,除了读取到对方发送的数据,还要读取到对方的网络地址[ip:port]std::string peerIp = inet_ntoa(peer.sin_addr);      //拿到了对方的IPuint32_t peerPort = ntohs(peer.sin_port);//拿到了对方的端口号checkOnlineUser(peerIp, peerPort, peer);//如果存在,就什么都不做,如果不存在,就添加该用户到在线列表中//打印出来客户端给服务器发送过来的消息logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);messageRoute(peerIp, peerPort, inbuffer);//消息路由:将收到的消息转发给所有人// sendto(_sockFd, outbuff, strlen(outbuff), 0, (const struct sockaddr*)&peer, len);}}void messageRoute(std::string ip, uint32_t port, std::string info){std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;for(auto& e : users){sendto(_sockFd, message.c_str(), message.size(), 0, \(const struct sockaddr*)&(e.second), sizeof(e.second));}}void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in& peer){std::string key = ip;key += ":";key += std::to_string(port);auto it = users.find(key);if(it == users.end())//没有找到{users.insert({key, peer});}}
private://服务器的sock fd信息int _sockFd;//服务器端口号信息uint16_t _port;//服务器ip地址std::string _ip;//onlineuserunordered_map<std::string, struct sockaddr_in> users;
};
//./udpServer port [ip]
int main(int argc, char* argv[])
{if(argc != 2 && argc != 3){Usage(argv[0]);exit(3);}uint16_t port = 0;std::string ip;port = atoi(argv[1]);if(argc == 3){   ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}

udpClient.cpp文件

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cassert>
#include<string>
#include<cstdlib>
#include<strings.h>
#include<pthread.h>
struct sockaddr_in server;
static void Usage(std::string name)
{std::cout << "Usage:\n\t" << name << "server_ip server_port" << std::endl;
}
void* recverAndPrint(void* args)
{while(true){int sockFd = *(int*)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);int s = recvfrom(sockFd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}
}
//udpClient server_ip server_port
//如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}//1.根据命令行,设置要访问的服务器IPstd::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);//2. 创建客户端//2.1 创建socketint sockFd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockFd > 0);//2.2 client不需要用户自己bind端口信息!因为OS会自动帮我们绑定(随机绑定一个未被使用的port),我们也最好这么做//所有的客户端软件<->服务器通信的时候,必须得有client[ip:port] <-> server[ip:client]//为什么呢?因为client很多,不能给客户端bind指定的port,port可能被别的client使用了,我们的client就无法使用了//那么server为什么要bind呢?server提供的服务,必须被所有人知道!并且server不能随便改变!//2.2 填写服务器对应的信息bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());pthread_t t;//只负责读数据并打印数据的线程pthread_create(&t, nullptr, recverAndPrint, (void*)&sockFd);//3. 通讯过程std::string buffer;while(true){std::cerr << "Please Enter# ";std::getline(std::cin, buffer);//发送消息给server//当客户端首次调用sendto的时候,我们的client会自动bind自己的ip和portsendto(sockFd, buffer.c_str(), buffer.size(), 0, \(const struct sockaddr*)&server, sizeof(server));}return 0;
}

Log.hpp文件

#pragma once
#include<cstdio>
#include<cstdarg>
#include<cassert>
#include<ctime>
#include<stdlib.h>
#include<string>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
const char* log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
void logMessage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);std::string name = getenv("USER");char logInfo[1024];va_list ap;//ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);//vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度va_end(ap);//ap = NULLFILE* out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr), \name.c_str() == nullptr ? "unknown" : name.c_str(),\logInfo);
}

Makefile文件

PHONY:all
all:udpClient udpServer
udpClient:udpClient.cppg++ -o $@ $^ -lpthread -std=c++11
udpServer:udpServer.cppg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -rf udpClient udpServer

问:为什么在本地练习的时候使用127.0.0.1?

答:因为当前是一台主机,并且是在本地进行测试代码,我们使用本地环回的方式来和当前主机进行通信,对内形成闭环。

tcp练习

Log.hpp文件
#pragma once
#include<cstdio>
#include<cstdarg>
#include<cassert>
#include<ctime>
#include<stdlib.h>
#include<string>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
const char* log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
void logMessage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);std::string name = getenv("USER");char logInfo[1024];va_list ap;//ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);//vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度va_end(ap);//ap = NULLFILE* out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr), \name.c_str() == nullptr ? "unknown" : name.c_str(),\logInfo);
}
util.hpp文件
#pragma once
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cassert>
#include<cstring>
#include<cstdlib>
#include<strings.h>
#include<unistd.h>
#include<ctype.h>
#include<pthread.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
#define SOCKET_ERROR 1
#define BIND_ERROR 2
#define LISTEN_ERROR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
Makefile文件
PHONY:all
all:tcpClient tcpServer
tcpClient:tcpClient.cppg++ -o $@ $^ -lpthread -std=c++11
tcpServer:tcpServer.cppg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -rf tcpClient tcpServer
tcpClient.cpp文件
#include"util.hpp"
static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " serverPort serverIp" << std::endl;std::cerr << "Example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;
}
volatile bool quit = false;
//.tcpClient serverIp serverPort
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t serverPort = atoi(argv[1]);std::string serverIp = argv[2];// 1.创建socket SOCK_STREAMint sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERROR);}// (2) 需要bind吗?需要,但是不需要自己显式的bind!// (3) 需要listen吗?不需要的。// (4) 需要accept吗?不需要的。// 2.connect,向服务器发起链接请求// 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 发起请求,connect会自动帮我们bindif(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;std::string message;while(!quit){message.clear();std::cout << "请输入你的消息# ";std::getline(std::cin, message);if(strcasecmp(message.c_str(), "quit") == 0){quit = true;//退出之后,服务端也能收到quit信息然后服务端也退出}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);return 0;
}
tcpServer.cpp文件(三个版本)
初始版本
#include"util.hpp"
#include"Log.hpp"class ServerTcp;//申明一下ServerTcpclass ServerTcp
{
public:ServerTcp(uint16_t port, const std::string& ip = ""):_listenSockFd(-1),_port(port),_ip(ip){}~ServerTcp(){}
public:void init(){//1. 创建socket_listenSockFd = socket(AF_INET, SOCK_STREAM, 0);if(_listenSockFd < 0){logMessage(FATAL, "socket:%s, %d", strerror(errno), _listenSockFd);exit(SOCKET_ERROR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), _listenSockFd);// 2. bind// 2.1 填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_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 本地socket信息写入对应的内核区if(bind(_listenSockFd, (const sockaddr*)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:%s ", strerror(errno));exit(BIND_ERROR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), _listenSockFd);// 3. 设置监听状态,监听socket。问:为什么要建立连接呢?答:因为TCP是面向连接的if(listen(_listenSockFd, 5) < 0){//listen监听失败logMessage(FATAL, "listen:%s, %d", strerror(errno), _listenSockFd);exit(LISTEN_ERROR);}//listen successlogMessage(DEBUG, "listen:%s, %d", strerror(errno), _listenSockFd);//允许别人连接了}void loop(){while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);//4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr*)&peer, &len);if(serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 5.提供服务, echo -> 小写 ->大写// 5.0 版本:只能处理单个客户端的请求transService(serverSockFd, peerIp, peerPort);// logMessage(DEBUG, "server 提供 service start.....");// sleep(1);}}// 大小写转化服务// TCP && UDP:支持全双工void transService(int sock, const std::string& clientIp, uint16_t clientPort){assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while(true){ssize_t  s = read(sock, inbuffer, sizeof(inbuffer) - 1);//ssize_t 就是long int//此处我们认为我们读到的都是字符串if(s > 0){//read successinbuffer[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);//无论是tcp还是udp都是同时对一个文件描述符写和读,说明tcp和udp通信是全双工write(sock, inbuffer, strlen(inbuffer));}else if(s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);}
private://sockint _listenSockFd;//portuint16_t _port;//ipstd::string _ip;
};
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];}ServerTcp svr(port, ip);svr.init();svr.loop();return 0;
}
5.1 V1版本(多进程版本)

第一个版本的tcpServer.cpp文件由于单进程的限制,所以只有一个客户端进程能够和服务端进程建立链接,下面对上面的代码进行优化(通过多进程的方式创建子进程来让子进城来处理服务端的任务,可以同时处理多个客户端的请求):

void loop()
{signal(SIGCHLD, SIG_IGN);//only linux:5.1版本代码,使用signal之后就不需要等待子进程了while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);//4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr*)&peer, &len);if(serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 5.提供服务, echo -> 小写 ->大写// 5.1 v1版本 -- 多进程版本pid_t id = fork();assert(id != -1);if(id == 0){// childclose(_listenSockFd);//子进程只是提供服务的,已经用不到这个文件描述符了transService(serverSockFd, peerIp, peerPort);exit(0);}//parentclose(serverSockFd);//父子进程是独立的两个进程,进程之间具有独立性,所以父进程关闭对子进程没有影响}
}
5.1 V2版本(多进程版本的另外一种写法)

多进程版本的另一种写法。

下面是对上面的代码进行优化:

void loop()
{while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);//4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr*)&peer, &len);if(serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 5.1 v2版本 -- 多进程版本// 爷爷进程pid_t id = fork();assert(id != -1);if(id == 0){// child:爸爸进程close(_listenSockFd);if(fork() > 0) exit(0);//爸爸进程终止//孙子进程 -- 没有了父进程 -- 变成了孤儿进程 -- 被系统领养 -- 回收问题交给了系统来回收transService(serverSockFd, peerIp, peerPort);exit(0);}//parentclose(serverSockFd);//父子进程是独立的两个进程,进程之间具有独立性,所以父进程关闭对子进程没有影响pid_t ret = waitpid(id, nullptr, 0);//阻塞式等待:爸爸进程终止,爷爷进程立马得到退出码,释放僵尸进程状态assert(ret > 0);(void)ret;}
}

下面是对上面的这一版本代码进行分析:

假设最开始的进程是爷爷进程,第一次fork创建的进程是爸爸进程,爸爸进程中fork创建的是孙子进程,爸爸进程终止之后,孙子进程变成了孤儿进程,孤儿进程就会被系统收养,让孤儿进程处理来自客户端的请求,爷爷进程就不需要再wait了。

5.2 多线程版本
class ThreadData
{
public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp* ts):_clientPort(port),_clientIp(ip),_sock(sock),_this(ts){}
public:uint16_t _clientPort;std::string _clientIp;int _sock;ServerTcp *_this;
};static void* threadRoutine(void* args)
{pthread_detach(pthread_self()); //设置线程分离ThreadData* td = static_cast<ThreadData*>(args);td->_this->transService(td->_sock, td->_clientIp, td->_clientPort);delete td;
}
void loop()
{while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);//4. 获取连接,accept的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr*)&peer, &len);if(serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 5.2 v2版本 -- 多线程版本// 问:这里不需要关闭文件描述符吗?// 答:不需要了,因为多线程会共享文件描述符表的。ThreadData* td = new ThreadData(peerPort, peerIp, serverSockFd, this);pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)td);}
}
线程池版本
Task.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include"Log.hpp"
using namespace std;
class Task
{
public://下面的这一行等价于typedef std::function<void(int, std::string, uint16_t)> callBack;using callBack_t = std::function<void(int, std::string, uint16_t)>;
public:Task():_sock(-1),_port(-1){}Task(int sock, std::string ip, uint16_t port, callBack_t func):_sock(sock),_ip(ip),_port(port),_func(func){}void operator()(){logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求 开始啦...", pthread_self(), _ip.c_str(), _port);_func(_sock, _ip, _port);logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求 结束啦...", pthread_self(), _ip.c_str(), _port);}~Task(){}
private:int _sock; //给用户提供IO服务std::string _ip; //client ipuint16_t _port; //client portcallBack_t _func; //回调方法
};
tcpServer.hpp文件
#include"util.hpp"
#include"Log.hpp"
#include"ThreadPool.hpp"
#include"Task.hpp"
class ServerTcp;//申明一下ServerTcp
class ThreadData
{
public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp* ts):_clientPort(port),_clientIp(ip),_sock(sock),_this(ts){}
public:uint16_t _clientPort;std::string _clientIp;int _sock;ServerTcp *_this;
};
class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string& ip = ""):_listenSockFd(-1),_port(port),_ip(ip),_tp(nullptr){}~ServerTcp(){}
public:void init(){//1. 创建socket_listenSockFd = socket(AF_INET, SOCK_STREAM, 0);if(_listenSockFd < 0){logMessage(FATAL, "socket:%s, %d", strerror(errno), _listenSockFd);exit(SOCKET_ERROR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), _listenSockFd);// 2. bind// 2.1 填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_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 本地socket信息写入对应的内核区if(bind(_listenSockFd, (const sockaddr*)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:%s ", strerror(errno));exit(BIND_ERROR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), _listenSockFd);// 3. 设置监听状态,监听socket。问:为什么要建立连接呢?答:因为TCP是面向连接的if(listen(_listenSockFd, 5) < 0){//listen监听失败logMessage(FATAL, "listen:%s, %d", strerror(errno), _listenSockFd);exit(LISTEN_ERROR);}//listen successlogMessage(DEBUG, "listen:%s, %d", strerror(errno), _listenSockFd);//允许别人连接了// 4.加载线程池_tp = ThreadPool<Task>::getInstance();}static void* threadRoutine(void* args){pthread_detach(pthread_self()); //设置线程分离ThreadData* td = static_cast<ThreadData*>(args);td->_this->transService(td->_sock, td->_clientIp, td->_clientPort);delete td;}void loop(){// 启动线程池_tp->start();logMessage(DEBUG, "thread pool start success, thread num: %d", _tp->GetThreadNum());while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);//4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr*)&peer, &len);if(serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 5.3 线程池版本// 5.3.1 构建任务Task t(serverSockFd, peerIp, peerPort, \std::bind(&ServerTcp::transService, this,\std::placeholders::_1, std::placeholders::_2, std::placeholders::_3 ));_tp->push(t);}}// 大小写转化服务// TCP && UDP:支持全双工void transService(int sock, const std::string& clientIp, uint16_t clientPort){assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while(true){ssize_t  s = read(sock, inbuffer, sizeof(inbuffer) - 1);//ssize_t 就是long int//此处我们认为我们读到的都是字符串if(s > 0){//read successinbuffer[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);//无论是tcp还是udp都是同时对一个文件描述符写和读,说明tcp和udp通信是全双工write(sock, inbuffer, strlen(inbuffer));}else if(s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);}
private:// sockint _listenSockFd;// portuint16_t _port;// ipstd::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];}ServerTcp svr(port, ip);svr.init();svr.loop();return 0;
}
ThreadPool.hpp文件
#include <iostream>
#include <queue>
#include <cstdlib>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include "Task.hpp"
#include "Log.hpp"
#include "Lock.hpp"
using namespace std;
const int gThreadNum = 5;
template <class T>
class ThreadPool
{
private:ThreadPool(int threadNum = gThreadNum): _isStart(false), _threadNum(threadNum){assert(_threadNum > 0);pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;ThreadPool &operator=(const ThreadPool<T> &) = delete;public:~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}int GetThreadNum(){return _threadNum;}
public:static void *threadRoutine(void *args){pthread_detach(pthread_self());ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (1){tp->lockQueue();while (!tp->haveTask()){tp->waitForTask();}//这个任务就被拿到了任务的上下文中T t = tp->pop();tp->unlockQueue();t(); // 让指定的线程处理这个任务}}void start(){assert(!_isStart);for (int i = 0; i < _threadNum; i++){pthread_t tmp;pthread_create(&tmp, nullptr, threadRoutine, this);}_isStart = true;}void push(const T &in){lockQueue();_taskQueue.push(in);choiceThreadForHandler();unlockQueue();}static ThreadPool<T> *getInstance(){static Mutex mutex;if (nullptr == instance) //仅仅是过滤重复的判断{LockGuard lockguard(&mutex); //进入代码块,加锁,退出代码块,自动解锁if (nullptr == instance){instance = new ThreadPool<T>();}}return instance;}private:T pop(){T tmp = _taskQueue.front();_taskQueue.pop();return tmp;}void lockQueue() { pthread_mutex_lock(&_mutex); };void unlockQueue() { pthread_mutex_unlock(&_mutex); }bool haveTask() { return !_taskQueue.empty(); }void waitForTask() { pthread_cond_wait(&_cond, &_mutex); }void choiceThreadForHandler() { pthread_cond_signal(&_cond); }private:bool _isStart; //表示该线程池是否启动int _threadNum;queue<T> _taskQueue;pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *instance;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;

util.hpp文件

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cassert>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#define SOCKET_ERROR 1
#define BIND_ERROR 2
#define LISTEN_ERROR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
了解popen函数

函数说明:

popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外。

  • 如果 type 为 r,那么调用进程读进 command 的标准输出。
  • 如果 type 为 w,那么调用进程写到 command 的标准输入。

返回值:

若成功则返回文件指针,否则返回NULL,错误原因存于errno中。

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
//参数介绍
command:我们想要执行的命令(字符串形式)
type:以什么形式执行。

下面使用popen函数让我们的客户端进程能够在服务端上执行相应的linux指令,然后将指令执行的结果返回给客户端。

Task.hpp文件:

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include"Log.hpp"
using namespace std;
class Task
{
public://下面的这一行等价于typedef std::function<void(int, std::string, uint16_t)> callBack;using callBack_t = std::function<void(int, std::string, uint16_t)>;
public:Task():_sock(-1),_port(-1){}Task(int sock, std::string ip, uint16_t port, callBack_t func):_sock(sock),_ip(ip),_port(port),_func(func){}void operator()(){logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求 开始啦...", pthread_self(), _ip.c_str(), _port);_func(_sock, _ip, _port);logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求 结束啦...", pthread_self(), _ip.c_str(), _port);}~Task(){}
private:int _sock; //给用户提供IO服务std::string _ip; //client ipuint16_t _port; //client portcallBack_t _func; //回调方法
};

tcpServer.cpp文件:

#include "util.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
class ServerTcp; //申明一下ServerTcp
class ThreadData
{
public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): _clientPort(port), _clientIp(ip), _sock(sock), _this(ts){}public:uint16_t _clientPort;std::string _clientIp;int _sock;ServerTcp *_this;
};
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); // ssize_t 就是long int//此处我们认为我们读到的都是字符串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;// }FILE *fp = popen(command, "r");if (fp == nullptr){logMessage(WARNING, "exec %s failed, because: %s", command, strerror(errno));break;}char line[1024];while (fgets(line, sizeof(line) - 1, fp) != nullptr){write(sock, line, strlen(line));}// dup2(sock, fp->_fileno);// fflush(fp);pclose(fp);logMessage(DEBUG, "[%s:%d] exec [%s] ...done", clientIp.c_str(), clientPort, command);}else if (s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);
}class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string &ip = ""): _listenSockFd(-1), _port(port), _ip(ip), _tp(nullptr){}~ServerTcp(){}public:void init(){// 1. 创建socket_listenSockFd = socket(AF_INET, SOCK_STREAM, 0);if (_listenSockFd < 0){logMessage(FATAL, "socket:%s, %d", strerror(errno), _listenSockFd);exit(SOCKET_ERROR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), _listenSockFd);// 2. bind// 2.1 填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_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 本地socket信息写入对应的内核区if (bind(_listenSockFd, (const sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:%s ", strerror(errno));exit(BIND_ERROR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), _listenSockFd);// 3. 设置监听状态,监听socket。问:为什么要建立连接呢?答:因为TCP是面向连接的if (listen(_listenSockFd, 5) < 0){// listen监听失败logMessage(FATAL, "listen:%s, %d", strerror(errno), _listenSockFd);exit(LISTEN_ERROR);}// listen successlogMessage(DEBUG, "listen:%s, %d", strerror(errno), _listenSockFd);//允许别人连接了// 4.加载线程池_tp = ThreadPool<Task>::getInstance();}static void *threadRoutine(void *args){pthread_detach(pthread_self()); //设置线程分离ThreadData *td = static_cast<ThreadData *>(args);td->_this->transService(td->_sock, td->_clientIp, td->_clientPort);delete td;}void loop(){// 启动线程池_tp->start();logMessage(DEBUG, "thread pool start success, thread num: %d", _tp->GetThreadNum());while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr *)&peer, &len);if (serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);// 提供服务:处理来自客户端的任务Task t(serverSockFd, peerIp, peerPort, execCommand);_tp->push(t);}}// 大小写转化服务// TCP && UDP:支持全双工void transService(int sock, const std::string &clientIp, uint16_t clientPort){assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // ssize_t 就是long int//此处我们认为我们读到的都是字符串if (s > 0){// read successinbuffer[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);//无论是tcp还是udp都是同时对一个文件描述符写和读,说明tcp和udp通信是全双工write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);}private:// sockint _listenSockFd;// portuint16_t _port;// ipstd::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];}ServerTcp svr(port, ip);svr.init();svr.loop();return 0;
}

tcpClient.cpp文件:

#include"util.hpp"
static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " serverPort serverIp" << std::endl;std::cerr << "Example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;
}
volatile bool quit = false;
//.tcpClient serverIp serverPort
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t serverPort = atoi(argv[1]);std::string serverIp = argv[2];// 1.创建socket SOCK_STREAMint sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERROR);}// (2) 需要bind吗?需要,但是不需要自己显式的bind!// (3) 需要listen吗?不需要的。// (4) 需要accept吗?不需要的。// 2.connect,向服务器发起链接请求// 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 发起请求,connect会自动帮我们bindif(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;std::string message;while(!quit){message.clear();std::cout << "请输入你的消息# ";std::getline(std::cin, message); //结尾不会有'\n'if(strcasecmp(message.c_str(), "quit") == 0){quit = true;//退出之后,服务端也能收到quit信息然后服务端也退出}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);return 0;
}
日志版代码

Log.hpp文件:

#pragma once
#include<cstdio>
#include<cstdarg>
#include<cassert>
#include<ctime>
#include<stdlib.h>
#include<string>
#include<fcntl.h>
#include<sys/stat.h>//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
#define LOGFILE "serverTcp.log"
const char* log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
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;
};
void logMessage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);std::string name = getenv("USER");char logInfo[1024];va_list ap;//ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);//vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度va_end(ap);//ap = NULLFILE* out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr), \name.c_str() == nullptr ? "unknown" : name.c_str(),\logInfo);fflush(out);// 将C缓冲区中的数据刷新到OSfsync(fileno(out)); //将OS中的数据尽快刷盘
}

tcpServer.hpp文件:

#include "util.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "daemonize.hpp"
class ServerTcp; //申明一下ServerTcp
class ThreadData
{
public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): _clientPort(port), _clientIp(ip), _sock(sock), _this(ts){}public:uint16_t _clientPort;std::string _clientIp;int _sock;ServerTcp *_this;
};
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); // ssize_t 就是long int//此处我们认为我们读到的都是字符串if (s > 0){command[s] = '\0';logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);FILE *fp = popen(command, "r");if (fp == nullptr){logMessage(WARNING, "exec %s failed, because: %s", command, strerror(errno));break;}char line[1024];while (fgets(line, sizeof(line) - 1, fp) != nullptr){write(sock, line, strlen(line));}// dup2(sock, fp->_fileno);// fflush(fp);pclose(fp);logMessage(DEBUG, "[%s:%d] exec [%s] ...done", clientIp.c_str(), clientPort, command);}else if (s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);
}class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string &ip = ""): _listenSockFd(-1), _port(port), _ip(ip), _tp(nullptr){_quit = false;}~ServerTcp(){if(_listenSockFd >= 0) close(_listenSockFd);}public:void init(){// 1. 创建socket_listenSockFd = socket(AF_INET, SOCK_STREAM, 0);if (_listenSockFd < 0){logMessage(FATAL, "socket:%s, %d", strerror(errno), _listenSockFd);exit(SOCKET_ERROR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), _listenSockFd);// 2. bind// 2.1 填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_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 本地socket信息写入对应的内核区if (bind(_listenSockFd, (const sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:%s ", strerror(errno));exit(BIND_ERROR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), _listenSockFd);// 3. 设置监听状态,监听socket。问:为什么要建立连接呢?答:因为TCP是面向连接的if (listen(_listenSockFd, 5) < 0){// listen监听失败logMessage(FATAL, "listen:%s, %d", strerror(errno), _listenSockFd);exit(LISTEN_ERROR);}// listen successlogMessage(DEBUG, "listen:%s, %d", strerror(errno), _listenSockFd);//允许别人连接了// 4.加载线程池_tp = ThreadPool<Task>::getInstance();}static void *threadRoutine(void *args){pthread_detach(pthread_self()); //设置线程分离ThreadData *td = static_cast<ThreadData *>(args);td->_this->transService(td->_sock, td->_clientIp, td->_clientPort);delete td;}void loop(){// 启动线程池_tp->start();logMessage(DEBUG, "thread pool start success, thread num: %d", _tp->GetThreadNum());while (!_quit){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接,accrpt的返回值是一个新的sock fdint serverSockFd = accept(_listenSockFd, (struct sockaddr *)&peer, &len);if(_quit){break;}if (serverSockFd < 0){// 获取链接失败logMessage(WARNING, "accept: %s[%d]", strerror(errno), serverSockFd);continue;}// 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s:[%d], sock fd: %d", strerror(errno), peerIp.c_str(), peerPort, serverSockFd);Task t(serverSockFd, peerIp, peerPort, execCommand);_tp->push(t);}}// 大小写转化服务// TCP && UDP:支持全双工void transService(int sock, const std::string &clientIp, uint16_t clientPort){assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (!_quit){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // ssize_t 就是long int//此处我们认为我们读到的都是字符串if (s > 0){// read successinbuffer[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);//无论是tcp还是udp都是同时对一个文件描述符写和读,说明tcp和udp通信是全双工write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// pipe:读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭// s == 0:代表对方关闭,即client退出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;}}//只要走到这,说明client一定退出了,服务到此结束close(sock); //将套接字关闭//如果一个进程对应的文件fd,打开了没有被关闭,文件描述符就会泄漏logMessage(DEBUG, "server close %d done", sock);}bool quitServer(){_quit = true;}
private:// sockint _listenSockFd;// portuint16_t _port;// ipstd::string _ip;// 引入线程池ThreadPool<Task> *_tp;// 安全退出bool _quit;
};
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;
}
ServerTcp* svrp = nullptr;
void sighandler(int signo)
{if(signo == 3 && svrp != nullptr) svrp->quitServer();logMessage(DEBUG, "server quit safe");
}
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];}signal(3, sighandler);//daemonize(); //将进程守护进程化,我们的进程就会变成守护进程Log log;log.enable();ServerTcp svr(port, ip);svrp = &svr;svr.init();svr.loop();return 0;
}

让服务端进程变成守护进程(精灵进程)

一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作,一旦用户启动之后,除非用户主动关闭,否则会在服务器上一直运行。

守护进程的特点

首先,守护进程最重要的特性是后台运行。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境通常是守护进程从执行它的父进程(特别是shell)继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,也可以由作业控制进程crond启动,还可以由用户终端(通常是shell)执行。

除了此点以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。

一般呈现给我们的有下面几个特点:

  • 父进程必须是系统

image-20221206091045303

image-20221206093237338

所以一般会有下面的两种说法:

  • 在命令行中启动一个进程
  • 在会话中启动一个进程组(可以一个),来完成某种任务

注意:所有会话内的进程fork创建子进程,一般而言依旧属于当前会话

所以如果我们想要创建一个真正的服务器进程,就必须让进程自成一个进程组,自成新会话,这样在客户端退出的时候,独立的会话依旧运行在服务器上,这种独立的进程我们称其为守护进程,也叫精灵进程。

进程变成守护进程的几种方式

1. 自己编写函数

#include <unistd.h>
pid_t setsid(void);
//返回值成功返回当前进程的新的sid,失败返回-1
//注意事项1. 当前进程不能是当前进程组的组长(可以成为当前进程组内的第二个进程)

选做:

//1.忽略SIGPIPE信号(管道:写端一直在写,读端关闭,写端会被信号终止:SIGPIPE)
//2.更改当前进程的工作目录(chdir)

必做:

//1.让子进程而不是主进程变成精灵进程
if(fork > 0) exit(0);
setsid();
//2.关闭(重定向)标准输入/标准输出/标准错误
//2.1 关闭
close(0,1,2); //很少有人这样做
//2.2 重定向(将三个流重定向到/dev/null(linux中的垃圾桶或者垃圾黑洞,凡是从这里面读写一律被丢弃))
// 注意:如果我们想将日志或者调试信息打印到文件中进行保存起来,我们也可以将0,1,2重定向到文件中
// 打开/dev/null,并且对0,1,2进程重定向

代码总结:

#pragma once
#include<cstdio>
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
void daemonize()
{//1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);//2. 更改进程的工作目录//chdir()//3. 让自己不要成为进程组的组长if(fork() > 0) exit(1);//4. 设置自己是一个独立的会话setsid();//5. 重定向0,1,2int fd = 0;if(fd = open("/dev/null", O_WRONLY) != -1)//如果不想重定向到文件黑洞中,此处也可以以读写的方式打开一个文件{dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);//6. 关闭掉不需要的fdif(fd > STDERR_FILENO){close(fd);}}
}

将上面的代码套到我们之前的代码中:

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];}daemonize(); //将进程守护进程化,我们的进程就会变成守护进程ServerTcp svr(port, ip);svr.init();svr.loop();return 0;
}

运行之后查看结果:

image-20221206141721477

出现了上面的结果说明调用成功。

这里了解一个函数fsync():

fflush(out); // 将C缓冲区中的数据刷新到OS
fsync(fd);   // 将OS中的数据尽快刷新到磁盘中

2. 使用系统接口

#include <unistd.h>
int daemon(int nochdir, int noclose);
//参数介绍
//nochdir当nochdir为0时,daemon将更改进城的根目录为root(/)//noclose当noclose为0是,daemon将进城的STDIN, STDOUT, STDERR都重定向到/dev/null。
//返回值成功返回0,失败返回-1并设置error

3. 使用命令行操作

语法格式:

nohup Command [ Arg … ] [ & ]

参数说明:

Command:要执行的命令。
Arg:一些参数,可以指定输出文件。
&:让命令在后台执行,终端退出后命令仍旧执行。

注意:

默认形成nohup.out日志文件来存储向标准输出和标准错误的内容。

实例:

以下命令在后台执行 root 目录下的 runoob.sh 脚本:

nohup /root/runoob.sh &

在终端如果看到以下输出说明运行成功:

appending output to nohup.out

参数说明:

  • a : 显示所有程序
  • u : 以用户为主的格式来显示
  • x : 显示所有程序,不区分终端机

TCP协议通讯流程

  • tcp是面向链接的,client connect && server accept
  • tcp在建立连接的时候,采用的是三次握手,在断开连接的时候,采用的是四次挥手
  • connect发起三次握手(client),server和client执行close()执行四次挥手中的两次
  • 下面感性理解一下三次握手和四次挥手

image-20221213111934047

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com