【计算机网络】socket 网络套接字

网络套接字

  • 一、端口号
    • 1. 认识端口号
    • 2. socket
  • 二、认识TCP协议和UDP协议
    • 1. TCP协议
    • 2. UDP协议
  • 三、网络字节序
  • 四、socket 编程
    • 1. socket 常见API
    • 2. sockaddr 结构
    • 3. 编写 UDP 服务器
      • (1)socket()
      • (2)bind()
      • (3)recvfrom()
      • (4)sendto()
      • (5)udp 服务端和客户端
    • 4. 地址转换函数
      • (1)相关接口
      • (2)关于 inet_ntoa
    • 5. 编写 TCP 服务器
      • (1)listen()
      • (2)accept()
      • (3)con
      • (4)守护进程
      • (5)tcp 服务端和客户端

一、端口号

1. 认识端口号

实际上我们两台机器在进行通信时,是应用层在进行通信,应用层必定会推动下层和对方的上层进行通信。

其实网络协议栈中的下三层,主要解决的是数据安全可靠的送到远端机器。而用户使用应用层软件,完成数据发送和接收的。那么用户要使用软件,首先需要把这个软件启动起来!所以软件启动起来,本质就是进程!所以两台机器进行通信,本质是两台机器之上的应用层在通信,也就是两个进程之间在互相交换数据!所以网络通信的本质就是进程间通信!只不过在网络通信中的公共资源是网络,通过网络协议栈利用网络资源,让两个不同的进程看到了同一份资源!

在网络协议栈中,在传输层怎么把数据正确交给上层应用层呢?怎么知道交给哪一个应用呢?所以就要求上层应用层和传输层之间必须协商一种方案,让我们把数据准确交给上层,这个方案我们称为端口号。所以在传输层的报头中,必须要有原端口号目的端口号,也就是根据目的端口号就可以决定这个数据的有效载荷要交给上层应用的哪一个!所以对于端口号无论对于客户端和服务端,都能唯一的标识该主机上的一个网络应用层的进程!

我们可以这样理解,其实在传输层当中,操作系统会形成一张哈希表,哈希表中的类型是 task_struct*,每一个应用层都要和该哈希表绑定端口号,本质就是根据端口号在哈希表里做哈希运算,如果该位置已经被占用了,就不能被绑定了,因为一个端口号只能被一个进程绑定;如果该位置没有被使用,就把该进程的pcb地址放在该位置上。

2. socket

因为在公网上,IP地址 能表示唯一的一台主机,端口号 port,用来标识该主机上的唯一的一个进程,所以 IP + port 就可以标识全网唯一的一个进程!那么我们在网络通信时,只需要在对应的报头上填上原IP目的IP原port目的port,就可以将报文交给另一个主机的进程,这种基于 IP + port 的通信方式,我们称为 socket.

那么端口号和进程pid有什么区别呢?进程pid也能标识一台主机上的唯一进程啊?因为首先,不是所有的进程都要通信,但是所有的进程都要有pid!其次是为了使系统和网络功能解耦!

二、认识TCP协议和UDP协议

下面我们先认识一下两个传输层协议:

1. TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;后面我们再详细讨论 TCP 的一些细节问题。

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

2. UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论。

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

三、网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

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

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

在这里插入图片描述

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

四、socket 编程

1. socket 常见API

				// 创建 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);

2. sockaddr 结构

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

在这里插入图片描述

  • IPv4IPv6 的地址格式定义在 netinet/in.h 中,IPv4地址用 sockaddr_in 结构体表示,包括16位地址类型, 16位端口号和32位IP地址.;
  • IPv4IPv6 地址类型分别定义为常数 AF_INETAF_INET6,这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容;
  • socket API 可以都用 struct sockaddr* 类型表示,在使用的时候需要强制转化成 sockaddr_in;这样的好处是程序的通用性,可以接收IPv4IPv6,以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数。

3. 编写 UDP 服务器

(1)socket()

下面我们编写一个 UDP 服务器。首先需要做的是创建套接字,使用到的接口是 socket()

在这里插入图片描述

第一个参数是我们创建的套接字的域,即使用 IPv4 的网络协议还是 IPv6 的网络协议,目前我们只需要关注这两个即可,如下图:

在这里插入图片描述

第二个参数表示当前 socket 对应的类型,也就是相当于这个套接字未来给我们提供什么服务,是面向字节流的还是面向用户数据报的,如下:

在这里插入图片描述

第三个参数表示的是协议类型,目前我们不需要传这个参数。

而返回值相当于是一个文件描述符,所以创建一个套接字的本质,在底层就相当于是打开一个文件,只不过以前的 struct file 指向的是键盘、显示器这样的设备;而现在指向的是网卡设备。

在这里插入图片描述

(2)bind()

创建套接字成功之后,接下来就要绑定端口号,使用到的接口是 bind(),如下:

在这里插入图片描述

其中第一个参数就是创建套接字时的返回值;第二个参数是一个结构体;第三个参数是结构体的长度。但是我们在网络套接字编程的时候不用第二个参数类型的结构体,这个结构体它只是设计接口用,我们实际用的是 sockaddr_in 类型的结构体,只需要在传参的时候进行强转即可。我们可以使用 bzero() 接口将该结构体清0;

我们是要使用 bind 来让套接字和我们往该结构体中填充的网络信息要关联起来,所以我们需要想该结构体中填充对应的字段。该结构体中有如下字段:

在这里插入图片描述

对应下图:

在这里插入图片描述

其中 sin_zero 为该结构体的填充字段,也就是这些字段不用填充,当作占位符即可;sin_addr 代表 ip 地址;sin_port 代表服务器所使用的端口号;sin_family 代表该结构体对应的网络协议类型,IPv4 或者 IPv6.

因为我们在给对方发送数据的时候,我们也一定需要让对方知道我们是谁,所以我们需要将端口号携带上,发送给对方,这样对方把数据处理完,就可以给我们响应回来。所以端口号是要在网络里来回发送的,也就是需要保证我们的端口号是网路字节序列,因为该端口号是要给对方发送的。所以这里我们就需要用到主机序列转网络序列的接口,由于端口号是两个字节,所以用到的接口为 htons()

在这里插入图片描述

由于我们用户一般用的都是点分十进制字符串风格的 IP 地址,也就是 0.0.0.0 这种风格,每个点分的范围是 0~255,每个字符一个字节,远远超过结构体中要求的 32 位 ip 地址,也就是四字节。所以我们需要将该字符串类型转换为 uint32_t 的类型,那么用到的接口是 inet_addr(),它的作用就是将字符串风格的 ip 地址转化为网络风格的 uint32_t 类型,如下图:

在这里插入图片描述

同端口号一样,IP 地址也需要保证是网络字节序列。那么它的返回值类型 in_addr_t 其实就是符合网路字节序列的 uint32_t 的类型。

上面我们已经把准备工作做好了,接下来我们就需要使用 bind() 接口进行绑定,本质就是把我们定义的 struct 结构体设置进内核,设置进指定的套接字内部。

(3)recvfrom()

接下来我们就需要在指定的一个套接字里获取数据内容,使用到的接口是 recvfrom(),如下图:

在这里插入图片描述

第一个参数就是网络文件描述符;第二个参数和第三个参数分别表示我们提供的缓冲区和它的长度,读到的数据就会放在缓冲区中;第三个参数设为0就是默认使用阻塞方式;最后两个参数又是熟悉的结构体,由于我们需要知道这些数据是谁给我们发的,因为我们有可能也要将数据给对方返回。所以最后两个参数其实是输出型参数。

返回值成功就是对应的长度,否则就是-1,如下:

在这里插入图片描述

(4)sendto()

将数据发送回给对方使用到的接口为 sendto(),如下:

在这里插入图片描述

参数和 recvfrom() 的参数类似,这里不再介绍了。而最后两个参数是输入型参数,我们要将数据发回给对方,首先需要知道对方是谁,而我们上面已经通过 recvfrom() 获取到了对方的结构体信息,所以直接使用该结构体信息即可。

(5)udp 服务端和客户端

其中通过使用上面的接口编写的一个简单的接收客户端的字符串信息,并进行简单的加工的 udp 服务器代码链接为:UDP.

其中 udp server 的代码如下:

				#pragma once
				
				#include <iostream>
				#include <string>
				#include <sys/types.h>
				#include <sys/socket.h>
				#include <netinet/in.h>
				#include <arpa/inet.h>
				#include <strings.h>
				#include <unistd.h>
				#include <cstring>
				#include <functional>
				
				#include "log.hpp"
				
				using func_t = std::function<std::string(const std::string&)>;
				//typedef std::function<std::string(const std::string&)> func_t;
				
				std::string default_ip = "0.0.0.0";
				uint16_t default_port = 8080;
				
				log lg;
				
				class UdpServer
				{
				public:
				    UdpServer(const uint16_t &port = default_port, const std::string &ip = default_ip)
				        : _port(port), _ip(ip), _isrunning(false), _sockfd(0)
				    {}
				
				    void Init()
				    {
				        // 1.创建 udp 套接字
				        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET == PF_INET
				        if (_sockfd < 0)
				        {
				            lg(Fatal, "socket create faild, sockfd: %d", _sockfd);
				            exit(1);
				        }
				        lg(Info, "socket create success, sockfd: %d", _sockfd);
				
				        // 2.绑定端口号
				        // 2.1 准备数据
				        struct sockaddr_in local;
				        bzero(&local, sizeof(local));
				        local.sin_family = AF_INET;
				        local.sin_port = htons(_port); // 主机序列转网络序列
				        // local.sin_addr.s_addr = inet_addr(_ip.c_str());   // 1.string -> uint32_t  2.保证uint32_t是网络序列
				        local.sin_addr.s_addr = htonl(INADDR_ANY);
				
				        // 2.2 开始bind
				        int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
				        if (n < 0)
				        {
				            lg(Fatal, "bind faild, errno: %d, err message: %s", errno, strerror(errno));
				            exit(2);
				        }
				        lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));
				    }
				
				    void Run(func_t func)
				    {
				        _isrunning = true;
				        char buffer[1024];
				        while (_isrunning)
				        {
				            // 记录客户端发来时的结构体信息
				            struct sockaddr_in client;
				            socklen_t len = sizeof(client);
				            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
				            if (n < 0)
				            {
				                lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));
				                continue;
				            }
				
				            buffer[n] = 0;
				            // 对数据进行简单的加工
				            std::string info = buffer;
				            std::string echo_string = func(info);
				
				            // 发送回给对方
				            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);
				        }
				    }
				
				    ~UdpServer()
				    {
				        if (_sockfd > 0)
				            close(_sockfd);
				    }
				
				private:
				    int _sockfd;
				    uint16_t _port;
				    std::string _ip;
				    bool _isrunning;
				};

udp client 的代码如下:

				#include <iostream>
				#include <cstdlib>
				#include <unistd.h>
				#include <strings.h>
				#include <sys/types.h>
				#include <sys/socket.h>
				#include <netinet/in.h>
				#include <arpa/inet.h>
				
				using namespace std;
				
				void Usage(string proc)
				{
				    cout << "\n\rUsage: " << proc << " serverip serverport\n" << endl;
				}
				
				int main(int argc, char* argv[])
				{
				    if(argc != 3)
				    {
				        Usage(argv[0]);
				        exit(0);
				    }
				    string server_ip = argv[1];
				    uint16_t server_port = stoi(argv[2]);
				
				    sockaddr_in server;
				    bzero(&server, sizeof(server));
				    server.sin_family = AF_INET;
				    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
				    server.sin_port = htons(server_port);
				    socklen_t len = sizeof(server);
				
				    // client 也需要 bind,只不过不需要用户显示 bind,一般由OS自由随机选择
				    // 系统会在首次发送数据的时候给我们bind
				    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
				    if(sockfd < 0)
				    {
				        cout << "socker error" << endl;
				        return 1;
				    }
				
				    string message;
				    char buffer[1024];
				
				    while(true)
				    {
				        cout << "Plase Enter@ ";
				        getline(cin, message);
				
				        // 发送数据
				        sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)&server, len);
				
				        // 当服务器进行简单的加工处理后会发送回来,此时客户端再次获取
				        sockaddr_in temp;
				        socklen_t size = sizeof(temp);
				
				        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (sockaddr*)&temp, &len);
				        if(s > 0)
				        {
				            buffer[s] = 0;
				            cout << buffer << endl;
				        }
				    }
				    close(sockfd);
				    return 0;
				}

main 函数:

				#include <iostream>
				#include <vector>
				#include <memory>
				#include <cstdio>
				
				#include "UdpServer.hpp"
				
				using namespace std;
				
				void Usage(string proc)
				{
				    cout << "\n\rUsage: " << proc << " port[1024+]\n" << endl;
				}
				
				// 处理字符串的方法
				string Handler(const std::string& str)
				{
				    string res = "Server get a message: ";
				    res += str;
				    cout << res << endl;
				
				    return res;
				}
				
				// 远程执行指令的方法
				string ExcuteCommand(const string& cmd)
				{
				    FILE* fp = popen(cmd.c_str(), "r");
				    if(nullptr == fp)
				    {
				        perror("popen");
				        return "error";
				    }
				
				    string result;
				    char buffer[4096];
				    while(true)
				    {
				        char* tmp = fgets(buffer, sizeof(buffer), fp);
				        if(tmp == nullptr) break;
				        result = buffer;
				    }
				    pclose(fp);
				    return result;
				}
				
				int main(int argc, char* argv[])
				{
				    if(argc != 2)
				    {
				        Usage(argv[0]);
				        exit(0);
				    }
				
				    uint16_t port = stoi(argv[1]);
				    unique_ptr<UdpServer> svr(new UdpServer(port, "127.0.0.1"));
				
				    svr->Init();
				    svr->Run(ExcuteCommand);
				
				    return 0;
				}

有关代码中的细节:

  • 有关 IP 地址

云服务器禁止直接bind公网ipbind ip 地址为0,表示的含义是任意地址绑定,这种是比较推荐的做法。当 IP 地址为 127.0.0.1 时,表示进行的是本地传输测试,不会进行跨网传输。

  • 有关 port

其中 0~1023 的端口号是系统内定的端口号,一般都要有固定的应用层协议使用,例如 http:80,https:443;所以我们一般绑端口号,一般绑1024以上的。

  • popen() 系统调用

popen() 是一个被封装起来的管道和子进程执行命令的应用。

在这里插入图片描述

它的第一个参数就是需要执行的命令,在底层它会帮我们进行 fork() 创建子进程,并让父子进程建立管道,然后让子进程把它的运行结果通过管道再返回给调用方。如果调用方想得到 command 指令的运行结果,可以通过文件指针的方式读取。第二个参数相当于是打开这个命令的方式,我们使用 “r” 即可。使用完毕后使用 pclose() 关闭该文件指针即可。

其中,我们可以使用 netstat -nlup 查看系统中所有的 udp 信息,并且把进程信息也显示出来。

我们还可以将以上代码修改成为多线程代码,链接为:多线程UDP.

4. 地址转换函数

(1)相关接口

我们只介绍基于 IPv4socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示32位 的 IP 地址,但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换。我们在上面的 bind() 中也使用了地址转换函数 inet_addr().

  • 字符串转 in_addr 的函数:

      			#include <arpa/inet.h>
      			
      			int inet_aton(const char* strptr, struct in_addr* addrptr);
      			in_addr_t inet_addr(const char* strptr);
      			int inet_pton(int family, const char* strptr, void* addrptr);
    
  • in_addr 转字符串的函数:

      			char* inet_ntoa(struct in_addr inaddr);
      			const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
    

其中 inet_ptoninet_ntop 不仅可以转换 IPv4in_addr,还可以转换 IPv6in6_addr,因此函数接口是 void* addrptr.

(2)关于 inet_ntoa

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

在这里插入图片描述

man 手册上说,inet_ntoa 函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。

5. 编写 TCP 服务器

(1)listen()

TCP 是面向连接的,服务器一般是比较被动的,所以服务器一直处于一种等待连接到来的状态,这个工作叫做监听状态,使用到的接口是 listen(),如下:

在这里插入图片描述

第一个参数为指定的套接字,通过该套接字等待新连接的到来。第二个参数我们后面再介绍,暂时设为10左右即可。返回值,成功返回0,失败返回-1.

(2)accept()

因为 TCP 是面向连接的,所以在正式通信之前,先要把连接建立起来,使用到的接口为 accept(),该接口的作用是获取一个新的连接,如下:

在这里插入图片描述

第一个参数为我们刚刚设置为监听状态的套接字;后两个参数和 recvfrom() 的后两个参数一样,都是输出型参数,也就是谁给我们发的 TCP 报文,那么对应的套接字信息就会通过这两个参数返回出来。

而返回值成功返回一个文件描述符;否则返回-1;那么返回值也是一个文件描述符,我们原本也有一个文件描述符,为什么会有两个 sockfd 呢?我们该用哪个呢?其实它们分工是明确的,我们原本定义的 sockfd,即被创建的,被 bind 的,被监听的套接字,它的工作是从底层获取新的连接;而未来真正提供通信服务的,是 accept() 返回的套接字!

至此,我们可以使用 telnet 进行指定服务的一个远程连接,后面跟上 IP 地址和端口号即可;它在底层默认使用的就是 TCP.

(3)con

由于在 TCP 中,客户端是要连接服务器的,所以服务端需要有一个能够向服务器发起连接的接口,该接口为 connect(),如下:

在这里插入图片描述

该接口的作用是通过指定的套接字,向指定的网络目标地址发起连接。后两个参数和 sendto() 的后两个参数一样。返回值成功返回0,失败返回-1.

TCP 客户端也需要 bind,但是和 UDP 一样,不需要显示的 bind,系统会在客户端发起 connect 的时候,进行自动随机 bind.

我们可以使用 netstat -nltp 查看系统中所有 TCP 的信息,并把进程信息显示出来。

(4)守护进程

在我们登录 Linux 的时候,Linux 系统会给我们形成一个会话,而且会为每个会话创建一个 bash 进程,这个 bash 就可以为用户提供命令行服务。每个会话中只能存在一个前台进程,但是可以存在多个后台进程,而键盘信号只能发送给前台进程。前台和后台进程的区别就是是否拥有键盘文件,它们都可以向显示器打印,而只有前台进程才能从键盘,即标准输入获取数据!

如果我们不想后台进程向显示器打印的数据影响我们,我们可以将它的打印数据重定向到文件中,例如:

在这里插入图片描述

其中 [1] 表示后台任务号,后面数字表示进程 PID.

而查看后台任务的指令为:jobs,如下:

在这里插入图片描述

如果我们想把后台进程提到前台,可以使用 fg 任务号,如下:

在这里插入图片描述

如果想把它重新放回后台,我们可以使用 ctrl + z 将该进程暂停。然后使用 bg 任务号 将该进程重新启动,如下:

在这里插入图片描述

接下来我们再运行几个后台进程,例如使用 sleep,方便观察 Linux 中的进程间关系,使用 ps axj | head -1 && ps axj | grep -Ei 'a.out|sleep' 查看它们的进程信息:

在这里插入图片描述

其中 PPID、PID 我们都认识,而 PGID 表示的是进程组IDSID 表示 session id,即会话 id.

而系统中可能会存在多个 session,所以系统需要管理多个 session.

我们可以看到,./a.out 进程的 PIDPGID 是一样的,所以它就是自成进程组的。而三个 sleep 分别是三个不同的进程,但是它们的 PGID 却是同一个,而且是用管道建立的进程的第一个进程的 PID,所以它们三个自成一组,而组长是多个进程中的第一个。那么进程组和任务有什么关系呢?任务是要指派给进程组的!所以我们需要校正一下以前的说法,我们把前台进程称为前台任务,后台进程称为后台任务,因为可能某一个后台任务里面,可能会包含多个进程。但是无论有几个进程组完成对应的任务,在同一个会话内启动的,SID 是一样的!那么上面中的 SID 到底是谁呢?我们可以查看一下:

在这里插入图片描述

如上图,我们可以看到,它是 bash!所以就是以 bashpid 去构建了一个 session

这种后台进程会收到用户登录和退出的影响,如果我们不想受到任何用户登录和注销的影响,我们可以将进程守护进程化。什么是守护进程呢?我们把自成进程组自成会话的进程称为守护进程!那么我们该如何做到呢?下面我们认识一个接口:setsid(),如下:

在这里插入图片描述

该接口的作用就是,哪个进程调用该接口,就把该进程的组ID设置为会话ID,也就是让进程独立成会话。

在这里插入图片描述

返回值成功返回进程的ID,否则返回-1.

注意,该接口不能由进程组的组长直接调用,那么怎么才能保证不是组长调用呢?所以我们可以使用 fork() 创建子进程调用!所以守护进程的本质,也是孤儿进程!

(5)tcp 服务端和客户端

接下来我们结合上面所学的知识,编写一个 TCP 服务器,并将它守护进程化,代码链接:

其中在守护进程中,我们的代码中是充满大量的打印的,而这些打印默认是向标准输出打的,也就是向显示器上打了,而对于守护进程来说,就不应该向显示器上打了,所以我们需要一个解决方案。而 Linux 中存在一种字符文件,叫做 /dev/null,只要我们向该文件写入,都会被该文件丢弃掉,如果我们向该文件读取,什么也读取不到。所以我们只需要将所有的输出向该文件写入即可。我们也可以将打印信息写入文件中。

另外,TCP 在通信时是全双工的,也就是可以同时读写的。在底层操作系统给 TCP 提供两个缓冲区,一个发送缓冲区,一个接收缓冲区,我们在用 TCP 的同时,别人也在用,所以别人也会有上面两个缓冲区,所以当我们发送数据,是先把我们的数据拷贝到我们的 TCP 的发送缓冲区,然后通过网络会发送到对方的接收缓冲区,反过来也同理,如下图:

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

170基于matlab的DNCNN图像降噪

基于matlab的DNCNN图像降噪&#xff0c;网络分为三部分&#xff0c;第一部分为ConvRelu&#xff08;一层&#xff09;&#xff0c;第二部分为ConvBNRelu&#xff08;若干层&#xff09;&#xff0c;第三部分为Conv&#xff08;一层&#xff09;&#xff0c;网络层数为17或者20层…

平衡二叉树(AVL),“平衡”是指什么?为什么要“平衡”?

一、“平衡因子”是什么&#xff1f; 定义&#xff1a;某节点的左子树 与 右子树的高度(深度)差&#xff0c;即为该节点的平衡因子&#xff08;BF,Balance Factor&#xff09;。 二、 原文链接&#xff1a;https://blog.csdn.net/kexuanxiu1163/article/details/103080901 …

指针的进阶(C语言)(下)

目录 4、数组参数、指针参数传参 4.1一维数组传参 4.2二维数组传参 4.3 一级指针传参 4.4 二级指针传参 5、函数指针 6、函数指针数组 7、指向函数指针数组的指针 8、回调函数 总结 续上篇 4、数组参数、指针参数传参 在写代码的时候难免把【数组】或者【指针】传给…

MySQL 多表操作

一.多表关系 1.一对一关系 一个学生只有一张身份证&#xff1b;一张身份证只能对应一个学生。 在任一表中添加外键&#xff0c;指向另一方主键&#xff0c;确保一对一关系。 一般一对一关系很少见&#xff0c;遇到一对一关系的表最好合并。 2.一对多/多对一关系 一个部门…

ArcgisForJS如何访问Arcgis Server?

文章目录 0.引言1.准备ArcGIS相关工具2.创建含有ArcSDE地理数据库的MXD文件3.注册ArcSDE地理数据库4.发布数据到Arcgis Server5.ArcgisForJS访问ArcGIS Server数据 0.引言 ArcGIS API for JavaScript 是一个用于在Web和移动应用程序中创建交互式地图和地理空间分析应用的库。它…

抽象工厂模式 Abstract Factory

1.模式定义: 提供一个创建一系列相关或互相依赖对象的接口&#xff0c;而无需指定它们具体的类 2. 应用场景: 程序需要处理不同系列的相关产品&#xff0c;但是您不希望它依赖于这些产品的 具体类时&#xff0c; 可以使用抽象工厂 3.优点: 1.可以确信你从工厂得到的产品彼…

恒峰-智能高压森林应急消防泵:安全护林新利器

随着科技的发展&#xff0c;人类对自然资源的保护意识日益增强。森林作为地球上最重要的生态系统之一&#xff0c;对于维护生态平衡、净化空气、保持水源等方面发挥着举足轻重的作用。然而&#xff0c;森林火灾却时常威胁着这片绿色家园。为了更好地保护森林资源&#xff0c;智…

D5020——外围元件少,内含压缩器和扩展器静噪电路,可应用在1.5V立体声耳机上,响应时间可调

D5020是一块增益可调 的压缩、扩展电路。它有两个通道组成&#xff0c;一个通道作扩展用&#xff0c;另一个通道能作压缩或扩展用。电路内部含有小信号全波整流、检测信号的大小&#xff0c;用于调节输入或反馈通道的增益大小。含有温度特性较好的带隙精密基准源&#xff0c;静…

二.西瓜书——线性模型、决策树

第三章 线性模型 1.线性回归 “线性回归”(linear regression)试图学得一个线性模型以尽可能准确地预测实值输出标记. 2.对数几率回归 假设我们认为示例所对应的输出标记是在指数尺度上变化&#xff0c;那就可将输出标记的对数作为线性模型逼近的目标&#xff0c;即 由此&…

五种多目标优化算法(NSWOA、MOJS、MOAHA、MOPSO、NSGA2)性能对比(提供MATLAB代码)

一、5种多目标优化算法简介 1.1NSWOA 1.2MOJS 1.3MOAHA 1.4MOPSO 1.5NSGA2 二、5种多目标优化算法性能对比 为了测试5种算法的性能将其求解9个多目标测试函数&#xff08;zdt1、zdt2 、zdt3、 zdt4、 zdt6 、Schaffer、 Kursawe 、Viennet2、 Viennet3&#xff09;&#xff0…

上进计划 | Python爬虫经典实战项目——电商数据爬取!

电商数据采集之——电商数据爬虫|电商数据采集API接口 电商数据爬虫背景 在如今这个网购风云从不间歇的时代&#xff0c;购物狂欢持续不断&#xff0c;一年一度的“6.18年中大促”、“11.11购物节”等等成为了网购电商平台的盛宴。在买买买的同时&#xff0c;“如何省钱&#…

昇腾ACL应用开发之模型转换ATC

一.前提条件 在前面的章节中我们已经安装了包含模型转换的套件包CANN-TOOLKIT&#xff0c;默认的安装路径会在/usr/local/Ascend里面&#xff0c;我们将该套件所需要的东西加入到环境变量中以便我们调用&#xff1a; 将source /usr/local/Ascend/ascend-toolkit/set_env.sh加入…

【鸿蒙系统学习笔记】TypeScript开发语言

一、背景 HarmonyOS 应用的主要开发语言是 ArkTS&#xff0c;它由 TypeScript&#xff08;简称TS&#xff09;扩展而来&#xff0c;在继承TypeScript语法的基础上进行了一系列优化&#xff0c;使开发者能够以更简洁、更自然的方式开发应用。值得注意的是&#xff0c;TypeScrip…

力扣 面试题 05.06. 整数转换

思路&#xff1a; 牵扯到二进制数&#xff0c;基本上要考虑位运算符&#xff0c;相关知识可以见http://t.csdnimg.cn/fzts7 之前做过类似的题目&#xff0c;大致思路就是先用按位异或^找出不同位&#xff0c;再用n&&#xff08;n-1&#xff09;计算出不同位的个数&#x…

nuxt项目搭建

1.先下载nuxt脚手架 yarn create nuxt-app <项目名>&#xff0c;记得安装完项目&#xff0c;npm i,下载node包 目录介绍 components 存放组件分别是头部&#xff08;包含导航&#xff09;和底部 layouts 页面布局&#xff0c;实现一个页面整体架构规则&#xff0c;头…

Sora 全网最全资料

大家好,本资料库是全网集体智慧的结晶,通过这个资料库,我们希望能够为读者提供一个全方位、多角度了解和研究Sora大模型的平台。每一部分都旨在深入探讨Sora大模型的不同方面,从技术细节到社会影响,再到未来展望,以确保读者能够获得最全面的信息和洞见。 📁一. 概念和…

yolov5导出onnx转engine推理

yolov5导出注意事项 配置 需要提供配置文件和权重文件&#xff0c;不然导出模型不能正常推理。 默认提供检测头。 ModuleNotFoundError: No module named ‘tensorrt’安装TensorRT-python发现报错 由于ModuleNotFoundError: No module named ‘tensorrt’安装TensorRT-pyt…

Android14 InputManager-InputManagerService环境的构造

IMS分为Java层与Native层两个部分&#xff0c;其启动过程是从Java部分的初始化开始&#xff0c;进而完成Native部分的初始化。 □创建新的IMS对象。 □调用IMS对象的start&#xff08;&#xff09;函数完成启动 同其他系统服务一样&#xff0c;IMS在SystemServer中的ServerT…

不要抱怨,不如抱 Java 运算符吧 (1)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

mplfinance 使用make_addplot做复杂股票走势图

mplfinance 使用make_addplot做复杂股票走势图 1.代码 import talib as tb import pandas as pd import mplfinance as mpfimport matplotlib.pyplot as pltplt.rcParams[font.sans-serif][simHei] # 以黑体显示中文 plt.rcParams[axes.unicode_minus]False # 解决保存图像符…