【Linux】网络编程套接字二

news2024/11/24 0:21:14

网络编程套接字二

  • 1.TCP网络编程
    • 1.1TCP Server服务端
    • 1.2 TCP Client客户端
  • 2.Server 多进程版本
    • 2.1普通版
    • 2.2 信号版
  • 3.Server 多线程版
  • 4.Server 线程池版
  • 5.日志函数重新设计
  • 6.守护进程
  • 7.TCP协议通讯流程
  • 8.TCP和UDP 对比

在这里插入图片描述

喜欢的点赞,收藏,关注一下把!在这里插入图片描述

1.TCP网络编程

TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们写代码的方式看一下。

1.1TCP Server服务端

#pragma once

#include <iostream>
#include <string>

using namespace std;

enum
{
    USAGG_ERR = 1,
};

class tcpServer
{
public:
    tcpServer(const uint16_t port) : _port(port), _listensock(-1)
    {
    }

    void initServer()
    {
    }

    void start()
    {
    }

    ~tcpServer()
    {
    }

private:
    uint16_t _port;
    int _sock;
};

我们已经知道,云服务器不允许绑定公网IP,所以这里我们直接使用INADDR_ANY绑定任意IP,端口号自己指定就行了。

#include"tcpServer.hpp"
#include<memory>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

// ./tcpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGG_ERR);
    }
    uint16_t serverport=atoi(argv[1]);

    unique_ptr<tcpServer> tsv(new tcpServer(serverport));
    tsv->initServer();
    tsv->start();

    return 0;
}

初始化服务器

进行网络通信首先要创建套接字。

在这里插入图片描述

不过今天这里,socket第二个参数我们要写成 SOCK_STREAM 对应TCP协议面向字节流。

void initServer()
{
    // 1.创建socket套接字
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0)
    {
		
    }

}

_sock<0,说明套接字失败,那就没有必须进行了,但是我们这里封装一个日志函数。日志是有日志等级的。

操作系统也是有日志的。

cat /var/log/messages   //查看日志

日志等级有些是warning,error,fatal等

在这里插入图片描述

未来我们也想这些消息是以不同等级显示出来的,必须还要以特定的格式显示出来。

#pragma once

#include<iostream>
#include<string>

#define DUGNUM  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

void logMessage(int level,const std::string& message)
{
    //[日志等级] [时间戳/时间] [pid] [message]
    //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
    std::cout<<message<<std::endl;
    //暂定
};

未来在输出消息的时候,消息都是规范化的。统一调用这个函数,可以往显示器上面打,也可以往文件中写。

这个日志函数不完整,我们先把TCP服务端客户端写完再来完善。

enum
{
    USAGG_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
};

void initServer()
{
    // 1.创建socket文件套接字对象
    _listensock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listensock < 0)
    {
        logMessage(FATAL, "socket create error");
        exit(SOCKET_ERR);
    }
    logMessage(NORMAL, "socker create success");

    // 2.bind 绑定自己的网络消息 port和ip
    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; // 任意地址bind,服务器真实写法

    if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "bind socket error");
        exit(BIND_ERR);
    }
    logMessage(NORMAL, "bind socket success");
 }     

前面UDP服务端初始这两步做完到这里就完了,但是TCP服务器是面向连接的,所以当别人给我发数据时候不能直接发数据,必须先和我建立连接,这就意味着服务器必须时时刻刻知道他向我发起连接请求。所以必须有第3步 设置socket 为监听状态(为了获取新连接)

在这里插入图片描述

backlog:底层全连接队列的长度,这个参数在后面TCP协议的时候说 。

static const int backlog = 5;

enum
{
    USAGG_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

void initServer()
{
    // 1.创建socket文件套接字对象
    _listensock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listensock < 0)
    {
        logMessage(FATAL, "socket create error");
        exit(SOCKET_ERR);
    }
    logMessage(NORMAL, "socker create success");

    // 2.bind 绑定自己的网络消息 port和ip
    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; // 任意地址bind,服务器真实写法

    if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "bind socket error");
        exit(BIND_ERR);
    }
    logMessage(NORMAL, "bind socket success");

     // 3.设置socket为监听状态
     if (listen(_listensock, backlog) < 0) // backlog  底层链接队列的长度
     {
         logMessage(FATAL, "listen socket error");
         exit(LISTEN_ERR);
     }
     logMessage(NORMAL, "listen socker success");
     
 }  

初始化服务器:1.创建socket ,2.bind ,3.设置socket 为监听状态

启动服务器

TCP不能直接发数据 ,因为它是面向连接的。通信之前必须要先获取连接,因此首先要获取新连接。

在这里插入图片描述

从这个sockfd这里获取新连接。

accept函数后两个参数和recvfrom是一模一样的,这两个参数的含义也是一样的都是输入输出型函数,将来谁连的我,远端的客户端的ip和port是多少。所以需要这两个参数把客户端消息获取上来。

在这里插入图片描述

这些都不重要,最重要的是accept的返回值

在这里插入图片描述

成功时这个函数会从已接受的socket返回一个文件描述符!失败返回-1错误码被设置。

这里问题就来了,调用accept它的返回值也是文件描述符,而我们自己也建立一个文件描述符,那这两个文件描述符是什么意思?

下面举个例子理解:
今天我和一群朋友去杭州西湖旅游,玩累了准备找个地方吃饭,假设来了一个地方都是卖鱼的,王家鱼庄、李家鱼庄、张家鱼庄等等。每一家鱼庄门口都有一个拉客的人,张三是王家鱼庄的门口拉客的人。我们走着走着张三过来了,小哥小哥你们要不要吃饭啊,我们这里的鱼都是从西湖打上来的。我们感觉可以试试,于是张三就带我和我的朋友到王家鱼庄,到了门口张三就向大厅呼唤李四过来接客把我们带进去,李四过来招呼我们,给我们倒水介绍特殊菜。当我们在享受李四给我们带来的服务时,张三去那了?张三自己有自己的业务,他把我们招呼过来之后,转头就走了,又跑到路边找下一位客人了。当我和我的朋友在吃饭的期间,发现我们周边越来越热闹了,张三带着客人来然后在门口喊着让其他人招呼客人。李四给我们提供服务,王五给别的客人提供服务等等。张三一直干着这一件事情。

张三 : 拉客
李四、王五、赵六。。。:提供服务

张三就相当于我们传给accept的创建好的文件描述符,
李四、王五、赵六。。。就相当于accept返回文件描述符

一个服务器可能被多个客户端来连接,李四、王五、赵六。。。每一个都是对应一个文件描述符对外提供服务的, 未来我们一旦建立好连接,服务器不能用创建好的文件描述符和客户端通信,就好比不能用张三给客人提供服务,而应该让accept的返回值文件描述符来给用户提供服务。

class tcpServer
{
public:
	//。。。

   void start()
   {
       for (;;)
       {
           // 4.获取新链接
           //这个结构体用来获取谁连接的我
           struct sockaddr_in peer;
           socklen_t len = (sizeof(peer));
           //这个sock 用来和client进行通信的文件描述符
           int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
           if (sock < 0)
           {
               logMessage(ERROR, "accpet error");
               continue;//获取连接失败,但不影响获取下一个连接
           }
           logMessage(NORMAL, "accpet a new link success);
           //cout << "sock: " << sock << endl;
	 }

	//。。。
private:
    uint16_t _port;
    //int _sock;
    int _listensock;//不是用来进行数据通信的,它是用来监听连接的到来,获取新连接的
};

接下来就用这个sock和客户端进行通信了

void start()
{
    for (;;)
    {
        // 4.获取新链接
        //这个结构体用来获取谁连接的我
        struct sockaddr_in peer;
        socklen_t len = (sizeof(peer));
        //这个sock 用来和client进行通信的文件描述符
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
        if (sock < 0)
        {
            logMessage(ERROR, "accpet error");
            continue;//获取连接失败,但不影响获取下一个连接
        }
        logMessage(NORMAL, "accpet a new link success);
        //cout << "sock: " << sock << endl;//可以看到新的文件描述符

        // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!
        // version 1 这里我们后面会写好几个版本。因此先写第一个简单版本
        serverIO(sock);//用这个函数对外提供服务
        close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
}

void serverIO(int &sock)
{
    char buffer[1024];
    while (true)
    {
        // 读
        ssize_t n = read(sock, buffer, sizeof(buffer));
        if (n > 0)//返回读到多少字节
        {
        	//目前我们把读到的数据当成字符串,截至目前
            buffer[n] = 0;
            cout << "recv message: " << buffer << endl;

            // 写
            string outbuffer = buffer;
            outbuffer += "server [respond]";
            write(sock, outbuffer.c_str(), outbuffer.size());
        }
        else if (n == 0)//读到文件末尾
        {
            // 代表clien退出
            logMessage(NORMAL, "client quit, me to!");
            break;
        }
    }
}

服务端完整代码

#pragma once

#include "logMessage.hpp"

#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

enum
{
    USAGG_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

const int backlog = 5;

class tcpServer
{
public:
    tcpServer(const uint16_t port) : _port(port), _listensock(-1)
    {
    }

    void initServer()
	{
	    // 1.创建socket文件套接字对象
	    _listensock = socket(AF_INET, SOCK_STREAM, 0);
	    if (_listensock < 0)
	    {
	        logMessage(FATAL, "socket create error");
	        exit(SOCKET_ERR);
	    }
	    logMessage(NORMAL, "socker create success");
	
	    // 2.bind 绑定自己的网络消息 port和ip
	    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; // 任意地址bind,服务器真实写法
	
	    if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
	    {
	        logMessage(FATAL, "bind socket error");
	        exit(BIND_ERR);
	    }
	    logMessage(NORMAL, "bind socket success");
	
	     // 3.设置socket为监听状态
	     if (listen(_listensock, backlog) < 0) // backlog  底层链接队列的长度
	     {
	         logMessage(FATAL, "listen socket error");
	         exit(LISTEN_ERR);
	     }
	     logMessage(NORMAL, "listen socker success");
	     
	 }

	void start()
	{
	    for (;;)
	    {
	        // 4.获取新链接
	        //这个结构体用来获取谁连接的我
	        struct sockaddr_in peer;
	        socklen_t len = (sizeof(peer));
	        //这个sock 用来和client进行通信的文件描述符
	        int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
	        if (sock < 0)
	        {
	            logMessage(ERROR, "accpet error");
	            continue;//获取连接失败,但不影响获取下一个连接
	        }
	        logMessage(NORMAL, "accpet a new link success);
	        //cout << "sock: " << sock << endl;//可以看到新的文件描述符
	
	        // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!
	        // version 1 这里我们后面会写好几个版本。因此先写第一个简单版本
	        serverIO(sock);//用这个函数对外提供服务
	        close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
	}


	void serverIO(int &sock)
	{
	    char buffer[1024];
	    while (true)
	    {
	        // 读
	        ssize_t n = read(sock, buffer, sizeof(buffer));
	        if (n > 0)//返回读到多少字节
	        {
	        	//目前我们把读到的数据当成字符串,截至目前
	            buffer[n] = 0;
	            cout << "recv message: " << buffer << endl;
	
	            // 写
	            string outbuffer = buffer;
	            outbuffer += "server [respond]";
	            write(sock, outbuffer.c_str(), outbuffer.size());
	        }
	        else if (n == 0)//读到文件末尾
	        {
	            // 代表clien退出
	            logMessage(NORMAL, "client quit, me to!");
	            break;
	        }
	    }
	}  

    ~tcpServer()
    {
    }

private:
    // string _ip;
    uint16_t _port;
    int _listensock;
};
netstat -nltp  //查看处于监听的TCP

1.2 TCP Client客户端

#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class tcpClient
{
public:
    tcpClient(const string &ip, const uint16_t &port)
        : _serverip(ip), _serverport(port), _sockfd(-1)
    {
    }

    void initClient()
    {
    }

    void run()
    {    
    }

    ~tcpClient()
    {
    }

private:
    string _serverip;
    uint16_t _serverport;
    int _sockfd;
};
#include"tcpClient.hpp"

#include<memory>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_ip local_port\n\n";
}

// ./tcpClient serverip serverport
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    unique_ptr<tcpClient> utc(new tcpClient(serverip,serverport));
    utc->initClient();
    utc->run();

    return 0;
}

初始化客服端

void initClient()
{
    // 1.创建socket套接字
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
        cerr << "socket fail" << endl;
        exit(2);
    }

    // 2.tcp的客户端要不要bind,要不要显示bind?  要bind,不需要显示bind
    // 要不是listen 监听? 
    // 要不要accept? 
}

因为客户端和服务端通信需要【源ip ,目的ip】,【源端口,目的端口】,所以要bind。但是不需要显示bind,因为如果bind特定的端口,如果两个客户端都bind一样的端口,谁先启动谁成功bind,另一个就不能启动了。

下一个问题,我们的客户端要不要listen?
不需要,服务器 listen是因为有人要连接它,客户端是发起连接的。

那客户端要不要accept?
不需要,服务器accept也是因为有人要连接它,客户端是是发起连接的。

那客户端到底要什么呢?
要发起连接!

发现连接我们写到启动客户端里

启动客户端

在这里插入图片描述

第一个参数通过那个套接字发起连接
第二个参数你要向那个ip和port的服务端发起连接
第三个参数是这个结构体的长度

在这里插入图片描述

以前在udp是第一次sendto发现没有bind会调用bind绑定ip和port,而tcp这里是在connect会帮bind。

void run()
{
    // 2.发起链接
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(_serverport);
    server.sin_addr.s_addr = inet_addr(_serverip.c_str());

    if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
    {
        cerr << "socker connect fail" << endl;
    }
    else
    {
        string msg;
        while (true)
        {
            // 发
            cout << "Please Enter# ";
            getline(cin, msg);
            write(_sockfd, msg.c_str(), msg.size());

            // 收
            char buffer[1024];
            ssize_t n = read(_sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                cout <<"server 回显: " <<buffer << endl;
            }
            else
            {
                break;
            }
        }
    }
}

客户端完整代码

#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class tcpClient
{
public:
    tcpClient(const string &ip, const uint16_t &port)
        : _serverip(ip), _serverport(port), _sockfd(-1)
    {
    }

    void initClient()
    {
        // 1.创建socket套接字
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            cerr << "socket fail" << endl;
            exit(2);
        }

        // 2.要不要bind,要不要显示bind?  要bind,不需要显示bind
        // 要不是listen 监听? 不需要
        // 要不要accept? 不需要
        // 自己是发送链接的一方
    }

    void run()
    {
        // 2.发起链接
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            cerr << "socker connect fail" << endl;
        }
        else
        {
            string msg;
            while (true)
            {
                // 发
                cout << "Please Enter# ";
                getline(cin, msg);
                write(_sockfd, msg.c_str(), msg.size());

                // 收
                char buffer[1024];
                ssize_t n = read(_sockfd, buffer, sizeof(buffer));
                if (n > 0)
                {
                    buffer[n] = 0;
                    cout <<"server 回显: " <<buffer << endl;
                }
                else
                {
                    break;
                }
            }
        }
    }

    ~tcpClient()
    {
    	//这里也可以不主动关,我们知道文件描述符的生命周期随进程,客户端进程退了会自动帮关的
        if(_sockfd >= 0) close(_sockfd);
    }

private:
    string _serverip;
    uint16_t _serverport;
    int _sockfd;
};

在这里插入图片描述

为什么服务这里打印出来的文件描述符是4呢?
因为默认打开三个文件,0,1,2被占了,3被listensock占了,所以这里打印的是4

netstat -ntap  //查看所有处于tcp的进程

在这里插入图片描述

ESTABLISHED :建立连接
我们确实看到客户端发起的连接已经被服务端看到了并且连接了。
这里的问题为什么有两条连接呢?正常情况下不是一条连接吗?
一般而言,TCP确实在查找的时候建立连接成功,只会有一条连接!!!
但是今天我们做测试,客户端和服务端是在一台机器上的!!!
如果是两台主机,你是服务端你看到的就是上面的,你是客户端你看到的是下面的。即便只有一条连接也是全双工的!

在这里插入图片描述
这里可以看到客户端关了服务端立马读到了,客户端在连这个文件又变成4了,这说明客户端一关闭服务端就将刚刚的文件描述符关了,关了之后你在连接我给你的还是4,此时文件描述符就被重复使用了。

在这里插入图片描述

注意看,当我又开一个客户端去连接然后给服务端发送消息的时候,服务端并不会显示,只有当我把上一个客户端关闭后,然后才获取到新连接,这个文件描述符还是4,才会把我发的消息接收。

这是因为刚才所写的服务器,我们获取一个新连接之后,然后进程就去serverIO提供死循环服务了。人家不退,服务器就一直在serverIO给人家提供服务。

在这里插入图片描述

那怎么能保证并发的,给多个人提供服务呢?

下面我们就把刚才写的版本改一下:
多进程两个版本,多线程版本,线程池版本。

2.Server 多进程版本

2.1普通版

在这里插入图片描述
获取新连接之后创建子进程,创建子进程,父进程的文件描述符会被子进程继承的,文件描述符所指的文件也都是一样的。所以说父进程曾经打开的listensock以及sock子进程都能看到。

创建子进程,让子进程对外提供服务。
这里要注意父进程的文件描述符被子进程继承下来了,但是父进程可是打开了多个文件描述符,所以子进程最少把自己的不需要的文件描述符关掉。

void start()
{
	for (;;)
	{
		// 4.获取新链接
		struct sockaddr_in peer;
		socklen_t len = (sizeof(peer));
		int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
		if (sock < 0)
		{
		   logMessage(ERROR, "accpet error");
		   continue;
		}
		// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);
		logMessage(NORMAL, "accpet a new link success,get new sock");
		cout << "sock: " << sock << endl;
		
		// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!
		
		// version2 多进程
		int fd=fork();
		if(fd == 0) //child
		{
			//关闭不需要的监听文件描述符
			close(_listensock);
		    serverIO(sock);
		    close(sock);
			exit(0);
		}
		//父进程
}

那父进程要干什么呢?
根据以前在进程哪里所学知识,父进程当然是要阻塞或者非阻塞等待回收子进程的资源了,否则子进程退出变成僵尸进程了,就造成内存资源泄漏了

在这里插入图片描述

但是这里要等待的时候,选择阻塞式等待还是非阻塞等待?

选择阻塞式等待,那不还是串行执行吗,属于脱裤子放屁多此一举创建子进程。选择非阻塞式等待,万一没有新连接来了一直在accept哪里等着连接,对子进程资源可能并没有回收干净造成内存资源泄漏。所以选择非阻塞式等待并不好!

如果非要让你阻塞式等待,要怎么做?

这里是这样做的,让子进程关闭listensock之后,子进程在创建一个子进程也就是孙子进程,让子进程退出!孙子进程提供服务。因为子进程退出了所以父进程等待会立马成功,然后继续向下执行代码。虽然父进程回收了子进程资源,但是并不影响孙子进程提供服务,等孙子进程提供完服务自己退出。你是孙子进程和父进程没有半毛钱关系(各管各儿子),孙子进程是一个孤儿进程,孤儿进程会被操作系统领养然后等它退了回收它。

void start()
{
	for (;;)
	{
		//。。。
		
		// version2 多进程
		int fd=fork();
		if(fd == 0) //child
		{
			//关闭不需要的监听文件描述符
			close(_listensock);
			if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养
		    serverIO(sock);
		    close(sock);
			exit(0);
		}
		//父进程
        pid_t ret=waitpid(fd,nullptr,0);
        if(ret > 0)
        {
            logMessage(NORMAL,"waitpid child success");
        }
}

完整代码

void start()
{
	for (;;)
	{
		// 4.获取新链接
		struct sockaddr_in peer;
		socklen_t len = (sizeof(peer));
		int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
		if (sock < 0)
		{
		   logMessage(ERROR, "accpet error");
		   continue;
		}
		// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);
		logMessage(NORMAL, "accpet a new link success,get new sock");
		cout << "sock: " << sock << endl;
		
		// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!
		
		// version2 多进程
		int fd=fork();
		if(fd == 0) //child
		{
			//关闭不需要的监听文件描述符
			close(_listensock);
			if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养
		    serverIO(sock);
		    close(sock);
			exit(0);
		}
		//父进程
        pid_t ret=waitpid(fd,nullptr,0);
        if(ret > 0)
        {
            logMessage(NORMAL,"waitpid child success");
        }
}

在这里插入图片描述

看到现在可以多个用户同时连接了。但是多进程并不是一个好方法,因此子进程要拷贝一份父进程的东西。

2.2 信号版

上面还需要父进程自己回收子进程的资源太麻烦,我们知道子进程退出并不是默默退出的,它会发17号信号,不过系统默认对这个信号是忽略。这些知识我们在信号哪里说过,不在叙述。

因此这里我们让子进程退出然后资源自动被回收。父进程自己忙自己的事情。

void start()
{
    //子进程退出自动被OS回收
    signal(SIGCHLD,SIG_IGN);
    
    for (;;)
    {
		//。。。
		
        // version2 多进程信号版
        int fd=fork();
        if(fd == 0)
        {
            close(_listensock);
            serverIO(sock);
            close(sock);
            exit(0);
        }
        
}

这里有个问题,子进程关闭了不用的listensock文件描述符,父进程要不要关闭sock文件描述符?

在这里插入图片描述

父进程没关sock文件描述符,客户端关闭后再连接,文件描述符是一直增长的状态。文件描述符终有用完的时候!

所以父进程一定要关闭提供服务的sock文件描述符,虽然父进程关闭sock但它不会造成文件关闭,因为有引用计数,等到引用计数到0的时候这个文件才会真正的关闭!

void start()
{
    //子进程退出自动被OS回收
    signal(SIGCHLD,SIG_IGN);
    
    for (;;)
    {
        // 4.获取新链接
        struct sockaddr_in peer;
        socklen_t len = (sizeof(peer));
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
        if (sock < 0)
        {
            logMessage(ERROR, "accpet error");
            continue;
        }
        // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);
        logMessage(NORMAL, "accpet a new link success,get new sock");
        cout << "sock: " << sock << endl;

        // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!

        // version2 多进程信号版
        int fd=fork();
        if(fd == 0)
        {
            close(_listensock);
            serverIO(sock);
            close(sock);
            exit(0);
        }
        //细节,子进程关闭父进程的,父进程关闭子进程的
        close(sock);      
}

在这里插入图片描述
这里可能会有端口绑定失败,原因在具体谈TCP协议再说!
我们先换个端口用。
在这里插入图片描述

3.Server 多线程版

现在我们想用线程来解决为多人提供服务。

创建新线程,那主线程和新线程之间多文件描述符的态度是什么?
这个sock文件描述符能不能被新线程看到呢?

能!它们共享同一份资源!这里也不用敢像多进程那样让父子进程关闭对应的文件描述符那样做。它们共享同一份资源!

新线程创建好了,主线程也要回收新线程的资源。以前用的是pthread_join,但是在后面我们学过可以使用pthread_deatch进行线程分离,主线程就不用等了。

剩下线程代码细节我们以前说过,这里不再细说。

class tcpServer;//声明

struct ThreadDate
{
    ThreadDate(int sock,tcpServer* tps)
        :_sock(sock),_tps(tps)
    {}

    int _sock;
    tcpServer* _tps;
};

class tcpServer
{
public:
{
	//。。/

	static void* start_routine(void* args)
	{
	    ThreadDate* td=static_cast<ThreadDate*>(args);
	    pthread_detach(pthread_self());//退出自动回收资源
	    td->_tps->serverIO(td->_sock);
	    close(td->_sock);
	    delete td;
	    td=nullptr;
	}
	
	
	void start()
	{
	    for (;;)
	    {
	        // 4.获取新链接
	        struct sockaddr_in peer;
	        socklen_t len = (sizeof(peer));
	        int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
	        if (sock < 0)
	        {
	            logMessage(ERROR, "accpet error");
	            continue;
	        }
	        // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);
	        logMessage(NORMAL, "accpet a new link success,get new sock");
	        cout << "sock: " << sock << endl;
	
	        // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!
	
	
	        //version3  多线程
	        pthread_t pid;
	        //把this指针和sock一起传过去,因此写个结构体
	        ThreadDate* td=new ThreadDate(sock,this);
	        pthread_create(&pid,nullptr,start_routine,td);
	    }
	}
	
	//。。。
}

4.Server 线程池版

思路是这样的,未来新连接来了,我们可以把新连接构成一个任务,然后放到线程池里,由线程池来进行统一处理。

线程池我们在 【liunx】线程池+单例模式+STL,智能指针和线程安全+其他常见的各种锁+读者写者问题 这里写过,并且做了封装,因此我们拿过来直接用。

线程封装

#pragma once
#include<iostream>
#include<string>
#include<pthread.h>
#include<functional>

class Thread
{
    typedef  std::function<void*(void*)> func_t;
private:
    //类内成员有隐藏的this指针,不加static就会报错!
    //但是我们又需要this指针,调用类的成员变量,因此把this传过来
    static void* start_routine(void* args)
    {
        Thread* _this=static_cast<Thread*>(args);//安全进行类型转换
        return _this->_func(_this->_args);//调用回调函数,不这样写也可以再写一个类内函数在调用
    }
public:
    Thread()
    {
        char namebuffer[64];
        snprintf(namebuffer,sizeof namebuffer,"thread-%d",_number++);
        _name=namebuffer;
    }

    //为什么这里参数不放在构造函数
    //因为我们等会想线程运行的时候,知道是那个线程在运行把_name也一起传过去
    void start(func_t func,void* args)
    {
        _func=func;
        _args=args;
        //这个函数不认识C++的function类,因此我自己写一个函数
        pthread_create(&_tid,nullptr,start_routine,this);
    }

    void join()
    {
        pthread_join(_tid,nullptr);
    }

    std::string threadname()
    {
        return _name;
    }

    ~Thread()
    {}

private:
    std::string _name;//线程名
    func_t _func;//回调函数
    void* _args;//回调函数参数
    pthread_t _tid;//线程ID

    static int _number;
};

int Thread::_number=1;

锁封装

#pragma once
#include<iostream>
#include<pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock):_lock(lock)
    {
        pthread_mutex_init(_lock,nullptr);
    }

    void lock()
    {
        pthread_mutex_lock(_lock);
    }

    void unlock()
    {
        pthread_mutex_unlock(_lock);
    }

    ~Mutex()
    {
        pthread_mutex_destroy(_lock);
    }
private:
    pthread_mutex_t* _lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock):_mutex(lock)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

任务封装

#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <unistd.h>
#include "logMessage.hpp"
using namespace std;

void serverIO(int sock)
{
    char buffer[1024];
    while (true)
    {
        // 读
        ssize_t n = read(sock, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "recv message: " << buffer << endl;

            // 写
            string outbuffer = buffer;
            outbuffer += " server[respond]";
            write(sock, outbuffer.c_str(), outbuffer.size());
        }
        else if (n == 0)
        {
            // 代表clien退出
            logMessage(NORMAL, "client quit, me to!");
            break;
        }
    }
    close(sock);//提供完自己关闭文件文件描述符
}

class Task
{
    typedef std::function<void(int)> func_t;

public:
    Task(){};

    Task(int sock, func_t func) : _sock(sock), _callback(func)
    {
    }

    void operator()()
    {
        _callback(_sock);
    }

private:
    int _sock;
    func_t _callback;
};

线程池单例封装

#pragma once
#include "Thread.hpp"
#include "Task.hpp"
#include <vector>
#include <queue>
#include "Mutex.hpp"
#include <mutex>

using namespace std;
const int maxcap = 3;

// 声明
template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadData(ThreadPool<T> *poolthis, const string &name) : _poolthis(poolthis), _name_(name)
    {
    }
    ~ThreadData()
    {
    }

public:
    ThreadPool<T> *_poolthis;
    string _name_;
};

template <class T>
class ThreadPool
{
private:
    // 线程调用的处理任务函数
    static void *handTask(void *args)
    {
        ThreadData<T> *td = static_cast<ThreadData<T> *>(args);

        while (true)
        {
            Task t;
            // RAII 风格加锁
            {
                // 构造时自动加锁,析构时自动结束
                // 局部变量生命周期这个代码块
                LockGuard lockguard(td->_poolthis->mutex());
                while (td->_poolthis->IsQueueEmpty())
                {
                    td->_poolthis->threadwait();
                }
                td->_poolthis->pop(&t);
            }
            t(); //执行任务     
        }
        delete td;
        return nullptr;
    }

private:
    void threadlock() { pthread_mutex_lock(&_lock); }
    void threadunlock() { pthread_mutex_unlock(&_lock); }
    void threadwait() { pthread_cond_wait(&_cond, &_lock); }
    void pop(T *out)
    {
        *out = _task_queue.front();
        _task_queue.pop();
    }
    bool IsQueueEmpty() { return _task_queue.empty(); }
    pthread_mutex_t *mutex()
    {
        return &_lock;
    }

    // 单例不是没有例,构造函数不能去掉,放在private就好了
    ThreadPool(int cap = maxcap) : _cap(maxcap)
    {
        // 初始化锁,条件变量
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);

        // 创建线程
        for (int i = 0; i < _cap; ++i)
        {
            _threads.push_back(new Thread()); // 创建线程并放在vector里
        }
    }

    // 去掉赋值,拷贝构造
    void operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;

public:
    // 启动线程
    // 在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数
    // 而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针
    // 因此我们写个类把线程名和this都传过去
    void run()
    {
        for (auto &thread : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, thread->threadname());
            thread->start(handTask, td);
            cout << thread->threadname() << " statr... " << endl;
        }
    }

    // 任务队列放任务
    void push(const T &in)
    {
        // 保证放任务是安全的,所以先加锁
        pthread_mutex_lock(&_lock);
        _task_queue.push(in);
        pthread_cond_signal(&_cond); // 队列中有任务就唤醒等待的线程去取任务
        pthread_mutex_unlock(&_lock);
    }

    ~ThreadPool()
    {
        // 销毁锁,条件变量
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

    // 获取单例
    // 成员函数可以调用静态成员和静态成员函数,反之不行
    static ThreadPool<T> *getInstance()
    {
        // 虽然没有并发问题了,但是还有一个小问题
        // 未来每一个线程进来都要lock,unlock
        // 因此在外面再加一个if判断,未来只要第一次实例化之后就不需要再加锁解锁了
        // 大家就可以并发了
        if (tp == nullptr)
        {
            _singlock.lock();
            if (tp == nullptr)
            {
                tp = new ThreadPool<T>();
            }
            _singlock.unlock();
        }

        return tp;
    }

private:
    int _cap;                  // 线程个数
    vector<Thread *> _threads; // 线程放在vector里进行管理
    queue<T> _task_queue;      // 任务队列
    pthread_mutex_t _lock;
    pthread_cond_t _cond;

    static ThreadPool<T> *tp;
    // c++11的锁
    static std::mutex _singlock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;

template <class T>
mutex ThreadPool<T>::_singlock;

Server 线程池版

void start()
{
    //线程池启动
    ThreadPool<Task>::getInstance()->run();
    logMessage(NORMAL, "Thread init success");

    for (;;)
    {
        // 4.获取新链接
        struct sockaddr_in peer;
        socklen_t len = (sizeof(peer));
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
        if (sock < 0)
        {
            logMessage(ERROR, "accpet error");
            continue;
        }
        // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);
        logMessage(NORMAL, "accpet a new link success,get new sock");
        cout << "sock: " << sock << endl;

        // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!

        //version4 线程池
        //放任务
        ThreadPool<Task>::getInstance()->push(Task(sock,serverIO));
    }
}

5.日志函数重新设计

前面我们只是把日志函数简单说了一下,现在加一些设计。

我们想用一下可变参数,未来调用这个函数的时候是准备像下面这样调用。
创建套接字成功,然后打印一下。就像printf函数一样,【日志等级】【时间戳】【pid】【格式化的消息】

在这里插入图片描述

// void logMessage(int level,const std::string& message)
// {
//     //[日志等级] [时间戳/时间] [pid] [message]
//     //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
//     std::cout<<message<<std::endl;
// };


void logMessage(int level,const char* format,...)
{
	 //[日志等级] [时间戳/时间] [pid] [message]
    //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
    
}

如果自己写比较麻烦

// void logMessage(DEBUG, "hello %f, %d, %c", 3.14, 10, 'C');
void logMessage(int level, const char *format, ...)
{
    // [日志等级] [时间戳/时间] [pid] [messge]
    // [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]
     va_list start; //start是一个指针   
     va_start(start);//让start指向可变参数列表第一个参数
     while(*p){//p指向format的位置,如h
         switch(*p)
         {
             case '%':
                 p++;
                 if(*p == 'f') arg = va_arg(start, float);//让start提取一个float类型
             ...
         }
     }
     va_end(start); //start指针变成nullptr
}

提可变参数列表参数,一般用下面的函数。

在这里插入图片描述

const char* level_to_string(int level)
{
    switch(level)
    {
        case DUGNUM: return "DUGNUM";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
    }
}

//时间戳变成时间
char* timeChange()
{
    time_t now=time(nullptr);
    struct tm* local_time;
    local_time=localtime(&now);

    static char time_str[1024];

    snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\
                    local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \
                    local_time->tm_min, local_time->tm_sec);

    return time_str;
}

void logMessage(int level,const char* format,...)
{
    //[日志等级] [时间戳/时间] [pid] [message]
    //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024
    //获取时间
    char* nowtime=timeChange();
    char logprefix[NUM];
    snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());

    //
    char logconten[NUM];
    va_list arg;
    va_start(arg,format);
    vsnprintf(logconten,sizeof logconten,format,arg);
    
    std::cout<<logprefix<<logconten<<std::endl
}

在这里插入图片描述

我们也可以把这些日志信息放到文件中去,这里我们使用C++对文件操作

#pragma once

#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>

#define DUGNUM  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"

const char* level_to_string(int level)
{
    switch(level)
    {
        case DUGNUM: return "DUGNUM";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
    }
}

//时间戳变成时间
char* timeChange()
{
    time_t now=time(nullptr);
    struct tm* local_time;
    local_time=localtime(&now);

    static char time_str[1024];

    snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\
                    local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \
                    local_time->tm_min, local_time->tm_sec);

    return time_str;
}


void logMessage(int level,const char* format,...)
{
    //[日志等级] [时间戳/时间] [pid] [message]
    //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024
    //获取时间
    char* nowtime=timeChange();
    char logprefix[NUM];
    snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());

    //
    char logconten[NUM];
    va_list arg;
    va_start(arg,format);
    vsnprintf(logconten,sizeof logconten,format,arg);

    //写到文件
    if(level == DUGNUM || level == NORMAL || level == WARNING)
    {
        std::ofstream oss1(LOG_NORMAL,std::ios_base::out|std::ios_base::app);

        oss1<<logprefix<<logconten<<std::endl;    
    }
    else
    {
        std::ofstream oss2(LOG_ERR,std::ios_base::out|std::ios_base::app);
        oss2<<logprefix<<logconten<<std::endl;   
    } 
};

在这里插入图片描述
在这里插入图片描述

6.守护进程

服务器启动没问题,但是我们把这个终端关掉,此时我们看到服务就没了。也就是说服务器启动了不能关闭xshell,一关就没了。

在这里插入图片描述

正常服务器肯定不是这样运行的。服务器启动之后不再受用户登录注销的影响,而服务器可以自由运行的,除非未来不想用它,Quit它。

在liunx中这种进程,叫做守护进程

我们xshell客户端连上远端的与服务器会有一个会话,会话内部会给我提供一个前台进程bash,然后用户在命令行中自由的启动前台或者后台的任务,在这个会话中,只允许一个前台任务,和0个或者多个后台任务。
在这里插入图片描述
后面加& ,将任务放到后台

在这里插入图片描述

这里打印出来的东西暂时不用管

在这里插入图片描述

然后我们在以后台方式启动几个任务

在这里插入图片描述

这就是对应的两个作业,作用编号1、2

在这里插入图片描述

然后我们查看当前进程sleep,可以看到PGID,前三个进程是一样的,后三个进程是一样的,并且都是第一个进程的PID,这里想表达的是它们分别属于不同进程组。相同PGID的是一个进程组,组长是第一个进程。然后每个组三个人合起来成为一个进程组干一个作业。

这里想说的是,任务(1、2、3)是由各个进程组来完成的。

在这里插入图片描述

这些后端任务都属于同一个会话,从SID全都是一样可以看到。会话是以bashID来命名这个会话的。

在这里插入图片描述

& 以后端方式起任务

jobs //查看当前会话

在这里插入图片描述

fg 2 //2号任务放前台

在这里插入图片描述

ctrl+z //暂停这个任务

一个任务在前台暂停了,立马会被放到后台

在这里插入图片描述

然后ls发现bash又回来了,这证明了有且只有一个前台任务。

在这里插入图片描述

把一个任务放前台,bash自动变后台,这也说明以前我们自己在以./ 启动任务,是把任务放到前台了,所以输入其他指令根本没用

在这里插入图片描述

bg 2 //启动2号任务

在这里插入图片描述

所以我们得到的结论就是,作业是可以前台转化的

这就是会话进程组作业之间的关系。

xshell登录的时候会建立这么一堆东西,那退出登录呢?
是不是所有任务都会自动清理。
在这里插入图片描述

所以我们要想不受用户登录注销的影响,当这个会话要派生任务的时候,我们只把任务放在后台是不够的,我们需要把任务独立出来,让它自成会话,自成进程组,和终端设备无关。

在这里插入图片描述

这样的任务以进程方式呈现,我们叫它守护进程!不受用户登录注销的影响,可以一直在进行运行,除非未来不想让它运行了。

那我们现在就来服务器进程守护进程化。

守护进程化有n多种方式,系统提供了一个函数。不过自己我们自己实现一个。
以后也建议用自己的。

在这里插入图片描述

一个进行想要自己变成守护进程,一定要调用setsid
谁调用这个函数,谁就自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长。

在这里插入图片描述

但是这里调用setsid不能随便调,要求调用setsid的进程不能是进程组组长。

在这里插入图片描述

#pragma once

#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


void deamonSelf(const char *curPath = nullptr)
{
    // 1.让调用进程忽略掉异常的信号

    // 2.如何让自己不是组长, setsid

    // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件

    // 4.可选: 进程执行路径发送更改

}

比如客户端给服务端发了一个消息,服务端收到消息然后请求完要给客户端回过去,可是正准备写回去客户端奔溃了,那么服务端此时就是像一个以及被关闭的文件描述符写入,这就如同读端关闭,写端再写没用意义,写端会收到SIGPIPE信号退出。

#pragma once

#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


void deamonSelf(const char *curPath = nullptr)
{
    // 1.让调用进程忽略掉异常的信号
    signal(SIGPIPE, SIG_IGN);

    // 2.如何让自己不是组长, setsid
    if (fork() > 0)//创建子进程,父进程退出
        exit(0);
    //子进程  --守护进程也叫精灵进程,本质就是孤儿进程的一种
    pid_t n = setsid();
    assert(n != 1);

    // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件


    // 4.可选: 进程执行路径发送更改

}

守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件。

守护进程和显示器和键盘等已经没关系了,它就是一个独立的在后端运行,只有通过网络端口的方式进行访问。默认会打开0,1,2,可以直接close但是特别简单除暴不合理万一有些日志没写到文件中而打印到显示器但是我们关闭了那不就出问题了吗,进程之间挂掉了。因此我们选择重定向。

linux中存在一个特殊的文件,这个文件就像一个黑洞 ,默认处理方式,凡是向这个文件中写入都统统都丢弃掉。你读我也不阻塞你什么也读不到

在这里插入图片描述
在这里插入图片描述

#pragma once

#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define DEV "/dev/null"

void deamonSelf(const char *curPath = nullptr)
{
    // 1.让调用进程忽略掉异常的信号
    signal(SIGPIPE, SIG_IGN);

    // 2.如何让自己不是组长, setsid
    if (fork() > 0)
        exit(0);
    pid_t n = setsid();
    assert(n != 1);

    // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
    int fd=open(DEV,O_RDWR);
    if(fd > 0)
    {
        //重定向
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }

    // 4.可选: 进程执行路径发送更改
    if(curPath) chdir(curPath);
}
#include"tcpServer.hpp"
#include<memory>
#include"daemon.hpp"

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

// ./tcpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGG_ERR);
    }
    uint16_t serverport=atoi(argv[1]);

    unique_ptr<tcpServer> tsv(new tcpServer(serverport));
    tsv->initServer();
    
	//守护进程
    deamonSelf();
    
    tsv->start();

    return 0;
}

现在这个服务端进程就变成守护进程了。

在这里插入图片描述

调用setsid,自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长

在这里插入图片描述

然后客户端随意访问,服务端没有任何反应,在后端自动给我们反应

在这里插入图片描述

而且日志信息也打印到对应的文件中

在这里插入图片描述

最神奇的是,我们把xshell关掉,还可以连接到这个服务器,这就把端口暴露给外部,自己写的业务别人就可以直接进行返回了。

在这里插入图片描述

这个服务器除非自己主动退出!不然一直在后台运行。

在这里插入图片描述

7.TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

在这里插入图片描述
服务端:

服务端首先创建套接字,bind绑定ip和port,然后调用listen设置sock为监听状态,一旦调用listen服务器就由关闭状态变成监听状态就允许客户端来连接了,然后调用accept获取连接。在TCP我们有两套文件描述符,一个创建套接字返回上来的listenfd只用来获取新连接,一个accept返回上来connfd是未来IOfd用它作为IO读取。

这里有个细节,accept是获取连接,并不是创建连接,所谓获取连接前提是底层已经帮我创建好了连接,然后在应用层调用accept把连接拿上来,仅此而已。

客户端:

客户端首先创建套接字,然后调用connect发起链接请求,并且在调用connect的时候OS自动帮我们绑定ip和port。

在TCP这里我们采用链接的方案叫做三次握手

connect是发起三次握手链接请求的,而真正三次握手建立链接是双方的OS自动完成的。

accept是获取链接的,链接建立好了才能获取链接,因此accept并不参与三次握手的任何细节。
也就是说上层不调用accept,三次握手依旧能完成。

获取链接了,然后客户端和服务端调用read,write等接口进行IO通信,而TCP是可靠性的,所以在发信息后对方会给ACK确认。

TCP保证可靠性和调用read、write没有任何关系,一方发信息对方给ACK确认是双方OS去完成的。 甚至这个发信息也和write和read没关系这个后面说。

曾经建立了连接,才会有未来断开连接。断开连接在TCP这里采用的是四次挥手

四次挥手的工作也是由双方OS自动完成的,和我们没用半毛钱关系,而我们决定的是什么时候四次挥手。close是上层调用触发四次挥手。

下面再进一步感性认识三次握手,四次挥手

所谓建立链接是什么?
就如一个男生喜欢一个女生,并不是喜欢他们就在一起了,男生要想和女生在一起就必须先去尝试追求一下。因此男生首先主动发起追求(主动发起连接),他问女生:你愿意做我女朋友吗?女生回答说:好啊,什么时候开始呢? 男生说:就现在把。自此双方三次握手建立成功。

那现实中男生女生在一起了,知道各自是对方男朋友女朋友究竟是在干什么呢?
一定是记下来了一些东西,比如知道他是你的男朋友,她是你的女朋友。所以双方才知道他是我的女朋友,她是我的女朋友。

因此建立链接并不是简单的做了这个动作,它是手段,真正的目的是在双方要各自维护好链接建立好相关的属性信息 。

现在有一个男生有很多女朋友,他要有记录每一个女朋友属性信息。那怎么办呢?
他就需要对每一个女朋友对象先描述,在组织起来。弄一个链接结构管理这些女朋友们。

一个服务器可能有很多客户端发起链接,服务端也需要对这些客户端的链接先描述,在组织!对这些链接用特定的数据结构管理起来。

链接的总结:建立链接是双方OS自动完成的,建立链接过程是双方为了维护链接而创立的内核数据结构,这个内核数据结构对象是要有成本的,这个成本体现在创建的时候要花的时间和空间。

断开链接:是把曾经建立好的链接信息释放掉

断开链接为什么叫四次挥手呢?可以这样理解。男女朋友在一起最后结婚了一起生活了10年,但最终被现实打败了,男生说:我们离婚把。女生说:好啊。然后过了3秒,女生说:你跟我离婚,我也要跟你离婚。男生说:好。这种叫做协商。建立链接是一方主动,所以我们需要三次握手建立链接。断开链接是双方的事情,就必须争得双方的同意。你跟我断开链接,我也要和你断开链接,这叫做协商少了任何一方都只能叫通知。又因为TCP是保证可靠性的,我给你说的话要保证你听到了,你给我说的话要保证我听到了,所以我给你协商时你给我做应答保证我给你做协商时你听到了,反之也一样。所以需要4次,也就是四次挥手。

8.TCP和UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Java基础揉碎]抽象类

目录 通过问题引出 介绍 关键点 细节 ​编辑 抽象类的最佳设计模式--模版设计模式 1.先用最容易想到的方法 2.分析问题&#xff0c;提出使用模板设计模式 通过问题引出 假如我们有个动物类, 动物都有eat吃的方法, 但是具体吃什么, 我们不知道, 因为是什么动物我们不知道…

绘制特征曲线-ROC(Machine Learning 研习十七)

接收者操作特征曲线&#xff08;ROC&#xff09;是二元分类器的另一个常用工具。它与精确度/召回率曲线非常相似&#xff0c;但 ROC 曲线不是绘制精确度与召回率的关系曲线&#xff0c;而是绘制真阳性率&#xff08;召回率的另一个名称&#xff09;与假阳性率&#xff08;FPR&a…

【爬虫框架pyspider】01-pyspider入门与基本使用

前言 前面我们把爬虫的流程实现一遍&#xff0c;将不同的功能定义成不同的方法&#xff0c;甚至抽象出模块的概念。如微信公众号爬虫&#xff0c;我们已经有了爬虫框架的雏形&#xff0c;如调度器、队列、请求对象等&#xff0c;但是它的架构和模块还是太简单&#xff0c;远远…

|行业洞察·碳纤维|《中国碳纤维行业现状与发展趋势-39页》

报告内容的详细解读&#xff1a; 1. 战略性新材料的重要性 碳纤维是一种轻质高强的高性能纤维材料&#xff0c;在航空航天、国防军工、高端装备制造等领域具有不可替代的作用。碳纤维的应用有助于减少能源消耗和降低碳排放&#xff0c;符合全球可持续发展的要求。 |趋势洞察…

Java增强for循环和foreach循环的误区

网上很多文章都在说增强for循环和foreach循环遍历时不能修改值&#xff0c;只能查看&#xff0c;其实是有区分条件的&#xff0c;不能修改值的是包装类&#xff0c;例如List<String>,引用类型是可以修改值的&#xff0c;例如对象集合。 使用增强for循环或者foreach循环遍…

李宏毅【生成式AI导论 2024】第6讲 大型语言模型修炼_第一阶段_ 自我学习累积实力

背景知识:机器怎么学会做文字接龙 详见:https://blog.csdn.net/qq_26557761/article/details/136986922?spm=1001.2014.3001.5501 在语言模型的修炼中,我们需要训练资料来找出数十亿个未知参数,这个过程叫做训练或学习。找到参数后,我们可以使用函数来进行文字接龙,拿…

解决“Pycharm中Matplotlib图像不弹出独立的显示窗口”问题

matplotlib的绘图的结果默认显示在SciView窗口中, 而不是弹出独立的窗口, 这样看起来就不是很舒服&#xff0c;不习惯。 通过修改设置&#xff0c;改成独立弹出的窗口。 File—>Settings—>Tools—>Python Scientific—>Show plots in toolwindow 将√去掉即可

一台日本原生ip站群服务器多少钱?

一台日本原生ip站群服务器多少钱&#xff1f;日本原生ip站群服务器的价格受到多个因素的影响。以下是一些主要的因素&#xff1a; 服务器配置&#xff1a;硬件配置越高&#xff0c;自然价格也越高。对于站群服务器来说&#xff0c;由于需要同时运行多个网站&#xff0c;因此配置…

Vue挂载全局方法

简介&#xff1a;有时候&#xff0c;频繁调用的函数&#xff0c;我们需要把它挂载在全局的vue原型上&#xff0c;方便调用&#xff0c;具体怎么操作&#xff0c;这里来记录一下。 一、这里以本地存储的方法为例 var localStorage window.localStorage; const db {/** * 更新…

学习JavaEE的日子 Day32 线程池

Day32 线程池 1.引入 一个线程完成一项任务所需时间为&#xff1a; 创建线程时间 - Time1线程中执行任务的时间 - Time2销毁线程时间 - Time3 2.为什么需要线程池(重要) 线程池技术正是关注如何缩短或调整Time1和Time3的时间&#xff0c;从而提高程序的性能。项目中可以把Time…

【tensorflow框架神经网络实现鸢尾花分类】

文章目录 1、数据获取2、数据集构建3、模型的训练验证可视化训练过程 1、数据获取 从sklearn中获取鸢尾花数据&#xff0c;并合并处理 from sklearn.datasets import load_iris import pandas as pdx_data load_iris().data y_data load_iris().targetx_data pd.DataFrame…

Flask学习(六):蓝图(Blueprint)

蓝图&#xff08;Blueprint&#xff09;&#xff1a;将各个业务进行区分&#xff0c;然后每一个业务单元可以独立维护&#xff0c;Blueprint可以单独具有自己的模板、静态文件或者其它的通用操作方法&#xff0c;它并不是必须要实现应用的视图和函数的。 Demo目录结构&#xf…

八大技术趋势案例(人工智能物联网)

科技巨变,未来已来,八大技术趋势引领数字化时代。信息技术的迅猛发展,深刻改变了我们的生活、工作和生产方式。人工智能、物联网、云计算、大数据、虚拟现实、增强现实、区块链、量子计算等新兴技术在各行各业得到广泛应用,为各个领域带来了新的活力和变革。 为了更好地了解…

利用Java代码混淆技术提升应用程序抗逆向工程能力

摘要 本文探讨了代码混淆在保护Java代码安全性和知识产权方面的重要意义。通过混淆技术&#xff0c;可以有效防止代码被反编译、逆向工程或恶意篡改&#xff0c;提高代码的安全性。常见的Java代码混淆工具如IPAGuard、Allatori、DashO、Zelix KlassMaster和yGuard等&#xff0…

Python人工智能:气象数据可视化的新工具

Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;在数据处理、科学计算、数学建模、数据挖掘和数据可视化方面具备优异的性能&#xff0c;这些优势使得Python在气象、海洋、地理、气候、水文和生态等地学领域的科研和工程项目中得到广泛应用。可以…

物联网实战--入门篇之(一)物联网概述

目录 一、前言 二、知识梳理 三、项目体验 四、项目分解 一、前言 近几年很多学校开设了物联网专业&#xff0c;但是确却地讲&#xff0c;物联网属于一个领域&#xff0c;包含了很多的专业或者说技能树&#xff0c;例如计算机、电子设计、传感器、单片机、网…

葵花卫星影像应用场景及数据获取

一、卫星参数 葵花卫星是由中国航天科技集团公司研制的一颗光学遥感卫星&#xff0c;代号CAS-03。该卫星于2016年11月9日成功发射&#xff0c;位于地球同步轨道&#xff0c;轨道高度约为35786公里&#xff0c;倾角为0。卫星设计寿命为5年&#xff0c;搭载了高分辨率光学相机和多…

Oracle存数字精度问题number、binary_double、binary_float类型

--表1 score是number(10,5)类型 create table TEST1 (score number(10,5) ); --表2 score是binary_double类型 create table TEST2 (score binary_double ); --表3 score是binary_float类型 create table TEST3 (score binary_float );实验一&#xff1a;分别往三张表插入 小数…

抖音视频关键词无水印下载软件|手机网页视频批量提取工具

全新视频关键词无水印下载软件&#xff0c;助您快速获取所需视频&#xff01; 随着时代的发展&#xff0c;视频内容已成为人们获取信息和娱乐的重要途径。为了方便用户获取所需视频&#xff0c;推出了一款功能强大的视频关键词无水印下载软件。该软件主要功能包括关键词批量提取…

【话题】AI大模型学习:理论、技术与应用探索

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读小5的系列文章&#xff0c;这是《话题》系列文章 目录 背景1. AI大模型学习的基础理论1.1 机器学习1.2 深度学习 2. AI大模型学习的技术要点2.1 模型结构设计2.2 算法优化2.3 大规模数据处理 3. AI大模型学习的应用场景3.1 自…