【linux】网络 - 传输层 -- socket(TCP/UDP)


socket

Socket(套接字)是计算机网络编程中的一个抽象概念,用于在不同计算机之间进行通信。它是一种通信机制,允许计算机上的进程通过网络进行数据传输。Socket可以视为一种特殊的文件描述符,通过它可以进行读取和写入操作,就像操作文件一样。

socket的组成:IP + port

  • IP负责找到网络里的唯一主机。
  • port(端口号) 负责找到该主机上的唯一进程。

ip地址:标识网络里的唯一主机
记住2个特殊的ip地址:
0.0.0.0:某个套接字绑定到 0.0.0.0 地址意味着该套接字将接受来自任何本地网络接口的连接。
127.0.0.1: 是本地回环地址,通常被称为 “localhost”。这个地址通常用于在本地主机上进行网络通信。在网络编程中,使用 127.0.0.1 地址可以实现在同一台计算机上的不同进程之间进行通信。

端口号的特点

  1. 端口号和进程pid的联系与区别:
    PID是操作系统为每个进程分配的唯一标识符,用于在系统中区分不同的进程;而端口号是用于在网络通信中标识不同应用程序或服务的数字标识符。二者对于进程都具有唯一性

  2. 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

  3. 在计算机网络中,一些常见的端口号默认被特定的服务或进程占用。
    端口 80:HTTP服务通常使用此端口来提供Web服务。
    端口 443:HTTPS服务通常使用此端口来提供加密的Web服务。
    端口 21:FTP(文件传输协议)服务器通常使用此端口来进行文件传输。
    端口 22:SSH(安全外壳协议)服务器通常使用此端口来进行安全的远程访问。
    端口 25:SMTP(简单邮件传输协议)服务器使用此端口来传输电子邮件。
    ……

预备知识

TCP和UDP 协议

在网络编程中,常用的协议(如TCP/IP、UDP等)都基于Socket来实现
下面简单介绍一下

TCP

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

UDP

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

简单的理解:
TCP相当于打电话,先要建立连接,保证传输安全,再进行通信。
UDP相当于发邮箱,不用先建立连接,直接发,至于数据是否丢失、出错等,UDP并不关心。如果出问题,直接重发。

UDP的不可靠并不是贬义,它仅仅是对UDP特点的描述。连接的可靠性是有代价的,需要维护可靠性,这意味TCP更复杂,而不可靠意味UDP很简单。根据不同的应用场景,选择合适的协议的。

网络字节序

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

大端:低字节序放在高地址端,高字节序放在低地址端
小端:低字节序放在低地址端,高字节序放在高地址端
简记:小小小
在这里插入图片描述

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

socket接口及辅助接口

// 创建 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,这个结构体是什么?

sockaddr

sockaddr(socket address)结构体是用于表示套接字地址的数据结构。网络底层协议不同,套接字结构体也不同。

套接字编程的种类:

  • 域间套接字:同一个机器通信
  • 原始套接字:绕过传输层,直接访问网络层
  • 网络套接字:网络通信

不同的结构体导致使用难度上升,于是设计者设计了一套统一的接口sockaddr。
在这里插入图片描述
sockaddr_in和sockaddr_un是具体我们使用的套接字结构体。当我们将它们的指针作为参数传给socket接口函数,强制转换为sockaddr*。
如下:

struct sockaddr_in local;
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));

在函数内部,函数会解析local的前2个字节,如果是AF_INET就表示使用IPv4的网络通信,如果AF_UNIX就表示使用本地通信。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

那为什么不像很多系统接口一样,使用void * 来作为参数类型呢?
因为那时候c语言还没有void*

下面介绍一下sockaddr_in
sockaddr_in 是在 Linux 中用于表示 IPv4 地址和端口的结构体
官方的定义如下:
在这里插入图片描述
简化一下:

struct sockaddr_in {
    unsigned short   sin_family;   // 地址族 (AF_INET)
    unsigned short   sin_port;     // 端口号
    struct in_addr  sin_addr;     // IPv4 地址
    char             sin_zero[8];  // 填充 0 的字节以使结构与 sockaddr 结构的大小相同
};
  • sin_family 字段表示地址族,对于 IPv4 地址来说,它的值通常是 AF_INET。
  • sin_port 字段表示端口号,以网络字节序存储 (大端字节序)。
  • sin_addr 是一个 struct in_addr 类型的结构体,用于存储 IPv4 地址。
  • sin_zero 是用于填充的额外字段,以使 sockaddr_in 结构体的大小与 sockaddr 结构体的大小相同。

udpserver和udpclient

udpserver
在这里插入图片描述

std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
class udpserver
{
public:
	 udpserver(uint16_t port = defaultport) :_port(port)
    {}
    ~udpserver()
    {
        close(_sockfd);
    }
    void init()
    {
    	//初始化服务器
	}
	void run()
	{
		//运行服务器
	}
private:
    int _sockfd; //socket 文件描述符
    std::string _ip = defaultip; //点分十进制的ip地址
    uint16_t _port; //端口号
};

初始化udp服务器

void init()
    {
    	// 1 create sockfd   
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        // 2 init sockadd_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清0
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); /
        // 3 bin socket
        bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
    }
  1. create sockfd

int socket(int domain, int type, int protocol);// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
参数:

  • domain: 指定通信协议族。常见的包括 AF_INET(IPv4),AF_INET6(IPv6),AF_UNIX(Unix 域套接字),等等。
  • type: 指定套接字的类型,常见的包括 SOCK_STREAM(字节流式套接字,用于面向连接的通信,如 TCP),SOCK_DGRAM(数据报套接字,用于无连接的通信,如 UDP),以及其他类型。
  • protocol: 指定协议。在大多数情况下,可以指定为 0,表示使用默认协议。对于某些特定的协议族和类型组合,需要指定具体的协议。

我们创建的是ipv4的udpserver, 因此使用如下:
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);

  1. init sockadd_in
    分别对3个成员进行初始化,注意细节:
  • port:网络通信采用的是大端字节序,如果你使用的小端存储的机器,需要将port改为大端字节序。但你怎么知道自己使用的机器是大端还是小端?难道自己还有判断一下?有辅助接口可以解决这个麻烦。
    在这里插入图片描述
  • ip:其一、地址不是直接赋给sin_addr, sin_addr类型定义如下:
struct in_addr sin_addr;

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

sin_addr的类型是in_addr的结构体,地址保存在in_addr的成员s_addr中,使用时要注意。
其二、由于用户使用的是点分十进制风格的ip地址(字符串),因此有辅助接口帮助我们进行转换。
在这里插入图片描述
使用如下:

local.sin_addr.s_addr = inet_addr(_ip.c_str());
 inet_aton(_ip.c_str(), &local.sin_addr);
  1. bind socket
    现在有了socket结构体和sock文件描述符,我们还需要绑定二者。
    在这里插入图片描述
    bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));

由此udpserver的核心工作就完成了,为什么?因为udp是无连接的,不用关心消息传输的问题。

运行udp服务器

udpserver简易收发消息代码:

void run()
    {
        char buffer[SIZE]; //缓存
        while(_isRunning)
        {
            buffer[0] = 0;
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&client, &len);
            buffer[n] = 0;
            //处理服务器收到的数据报
            std::string client_message = buffer;
            client_message += " Server has received";

            //发送数据报
            sendto(_sockfd, client_message.c_str(), client_message.size(), 0, (struct sockaddr* )&client, len);
        }
    }

recvfrom和sendto是udp收发数据报的函数。

recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:指定待接收数据的套接字描述符。
  • buf:指向用于存放接收数据的缓冲区。
  • len:指定接收数据缓冲区的大小。
  • flags:一组标志,通常设置为0。
  • src_addr:指向用于存放发送端地址信息的结构体指针。
  • addrlen:指向一个整数,用于指定 src_addr 结构体的大小。
  • 返回接收到的字节数

sendto

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:指向包含目标地址信息的结构体指针。
  • addrlen:指定 dest_addr 结构体的大小。
  • 返回实际发送的字节数。

完整的服务器代码,这里的log.hpp是我写的一个简易日志。

#include <iostream>
#include <string>
#include <memory>

#include <unistd.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "log.hpp"
extern logger log;

std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8080;

int size = 1024;

class udpserver
{
public:
    udpserver(uint16_t port = defaultport) :_port(port)
    {}
    void init()
    {
        
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            //log(Fatal, "socket creat fail. socket : %d", _sockfd);
            exit(1);
        }
        //log(Info, "socket creat success. socket: %d", _sockfd);
        // 2 create sockadd_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清0
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // uint32_t 需要将字符串 --> uint32_t, 使用函数 inet_addr
        // 或者s_addr = INADDR_ANY
        // 3 bin socket
        int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            //log(Fatal, "bind error: %d, err string: %s", errno, strerror(errno));
            exit(1);
        }
        //log(Info, "bind success, n = %d", n);
    }

    void run()
    {
        _isRunning = true;
        char buffer[size];
        while(_isRunning)
        {
            buffer[0] = 0;

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                //log(Warning, "recvfrom error, errno:%d, err string: %s", errno, buffer);
                exit(1);
            }
            buffer[n] = 0;
            //log(Info, "receive success");
            //处理服务器收到的报文
            std::string client_message = buffer;
            client_message += " Server has received";

            //发送数据包
            sendto(_sockfd, client_message.c_str(), client_message.size(), 0, (struct sockaddr* )&client, len);
        }
    }

    ~udpserver()
    {
        close(_sockfd);
    }
private:
    int _sockfd;
    std::string _ip = defaultip;
    uint16_t _port;
    bool _isRunning = false;
};

udp客户端

在这里插入图片描述

客服端代码:客户端通常不用手动bind套接字和sockfd
当客户端调用 socket() 函数创建套接字时,操作系统会为该套接字分配一个未使用的端口号(通常是一个临时端口号),并在后续的通信过程中,该端口号会被动态地绑定到套接字上。这样客户端就可以与服务器端建立连接并进行通信,而无需手动指定端口号。
相反,服务器端通常需要绑定一个固定的端口号,以便客户端可以知道在哪个端口监听服务器的连接请求。因此,服务器端通常需要调用 bind() 函数来指定要监听的端口号和 IP 地址

#include <iostream>
#include <unistd.h>
#include <string>

#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        cout << "Usage: ./client [serverip] [serverport]" << endl;
        return 0;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    std::string message;
    char buffer[1024];
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);
        
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr* )&server, len);

        buffer[0] = 0;
        struct sockaddr_in tmp;
        socklen_t tmp_len = sizeof(tmp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,(struct sockaddr*)&tmp, &tmp_len);

        buffer[n] = 0;
        cout << buffer << endl;

    }

    close(sockfd);
    return 0;
}

问题:ip,port需要主机序列转网络序列,那网络通信的内容需要吗?需要,但不需要手动完成,recfrom和sendto会自动转换。

tcpserver和tcpclient

在这里插入图片描述

class tcpserver
{
public:
    tcpserver(uint16_t port = defaultport) : port_(port)
    {
    }
    ~tcpserver()
    {
        close(listensockfd_);
    }
    void init(); 
    void run();
private:
    int listensockfd_;
    std::string ip_ = defaultip;
    uint16_t port_;
};

初始化服务器

void init()
    {
        //1. 创建监听描述符
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            sfw_log(Fatal, "socket creat fail. socket : %d", listensockfd_);
            exit(1);
        }
        sfw_log(Info, "socket creat success. socket: %d", listensockfd_);
        // 2 create sockadd_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清0
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &local.sin_addr);
        // 3 bin listensockfd
        if (bind(listensockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
        	//自己写的一个日志
            //sfw_log(Fatal, "bind error: %d, err string: %s", errno, strerror(errno));
            exit(1);
        }
        sfw_log(Info, "bind success");
        if (listen(listensockfd_, backlog))
        {
            //sfw_log(Fatal, "listen error");
            exit(1);
        }
        sfw_log(Info, "listen success, sockf:%d", listensockfd_);
    }

和udp的区别,再udp中只有一种套接字:通信套接字。
而在tcp中有两种套接字:监听套接字和通信套接字

  • 监听套接字:监听是否有客户端进行连接
  • 通信套件字:与客户端传输数据
int listen(int sockfd, int backlog);
  • backlog(连接队列的长度):这是一个整数参数,指定在连接队列中等待接受处理的连接请求的最大数量。如果连接队列已满,新的连接请求将会被拒绝或丢弃。具体的实现限制可能有所不同,但通常情况下这个参数的取值范围在 1 到 SOMAXCONN 之间,SOMAXCONN 是系统定义的连接队列的默认最大长度(128)
  • 返回值:0成功,-1失败

运行服务器

    void run()
    {
        //sfw_log(Info, "topserver is running……");
        while (1)
        {
            // 1. 获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                //sfw_log(Warning, "accept error, errno : %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(client.sin_port);
            char client_ip[32] = {0};
            inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
            //sfw_log(Info, "get a new link, sockfd:%d, client [%s] [%d]", sockfd, client_ip, client_port);
			
            // 2 通过新连接进行通信
            //方法很多
            //不要忘记close(sockfd)
        }
    }

accept函数

int accept(int listensockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 返回值:sockfd, 用来与客户端通信。

tcp服务器不会一直与客户端进行连接,不然如果客户端连接了,但什么也不做,会浪费服务器资源。因此服务器一般会对连接设定时间,或者完成客户端的请求后,立即断开连接。

tcp与客户端进行通信的方式有很多种:

  1. 单进程:直接让主执行流执行。但是这将导致tcp服务器一次只能为一个连接服务
// 2 通过新连接进行通信
// 单进程
service(sockfd, client_ip, client_port);//服务
close(sockfd);
  1. 多进程:让孙子进程执行。可以一次服务多个连接,但创建进程的代价较大,效率低
    为什么要让孙子进程执行?如果让子进程执行,父进程还需要等待子进程,
多进程
             pid_t id = fork();
             if(id == 0)
             {
                 //son
                 close(listensockfd_);
                 if(fork() > 0) exit(0);
                 //孙子进程,会由系统领养
                 service(sockfd, client_ip, client_port);
                 close(sockfd);
                 exit(0);
             }
             //father
             close(sockfd);
             //阻塞等待,但是阻塞等待,会导致父进程卡在这,不会重新accept
             //让孙子进程操作
             pid_t rid = waitpid(id, nullptr, 0);
             (void)rid;
  1. 多线程:让其他执行流执行。可以一次服务多个连接,效率较高。但每次都要新建线程
			ThreadData *td = new ThreadData(sockfd, client_ip, client_port);
            pthread_t tid;
            pthread_create(&tid, nullptr, service_routine, (void *)td);
  1. 线程池:多线程的升级,提前创建好几个线程,有连接就交给线程池。
    这里的TcpTask和ThreadPool自定义
            TcpTask t(sockfd, client_ip, client_port);
            ThreadPool<TcpTask>::GetInstance()->push(t);

实现一个线程池版本:

#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <functional>

#include <unistd.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <pthread.h>

#include <string.h>


#include <netinet/in.h>
#include <arpa/inet.h>

#include "threadpool.hpp"//线程池

std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;

int size = 1024;
int backlog = 128;

struct ThreadData
{
    ThreadData(int sd, std::string i, uint16_t p) : sockfd(sd), ip(i), port(p)
    {
    }
    ~ThreadData()
    {
        close(sockfd);
    }
    int sockfd;
    std::string ip;
    uint16_t port;
};


class tcpserver
{
public:
    tcpserver(uint16_t port = defaultport) : port_(port)
    {
    }
    ~tcpserver()
    {
        close(listensockfd_);
    }
    void init()
    {
        // 1. 转换为守护进程
        Daemon();
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            //sfw_log(Fatal, "socket creat fail. socket : %d", listensockfd_);
            exit(1);
        }
        //sfw_log(Info, "socket creat success. socket: %d", listensockfd_);
        // 2 create sockadd_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清0
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &local.sin_addr);
        // 3 bin socket
        if (bind(listensockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            //sfw_log(Fatal, "bind error: %d, err string: %s", errno, strerror(errno));
            exit(1);
        }
        //sfw_log(Info, "bind success");
        if (listen(listensockfd_, backlog))
        {
            //sfw_log(Fatal, "listen error");
            exit(1);
        }
        //sfw_log(Info, "listen success, sockf:%d", listensockfd_);
    }
    void run()
    {
        //sfw_log(Info, "topserver is running……");
        while (1)
        {
            // 1. 获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                //sfw_log(Warning, "accept error, errno : %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(client.sin_port);
            char client_ip[32] = {0};
            inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
            //sfw_log(Info, "get a new link, sockfd:%d, client [%s] [%d]", sockfd, client_ip, client_port);

            // 2 通过新链接进行通信
            TcpTask t(sockfd, client_ip, client_port);
            ThreadPool<TcpTask>::GetInstance()->push(t);         
        }
    }

private:
    int listensockfd_;
    std::string ip_ = defaultip;
    uint16_t port_;
};

线程池

#pragma once

#include <iostream>
#include <pthread.h>
#include <queue>
#include <vector>
#include <string>

#include "TcpTask.hpp"

/*
设计思路:

一、线程池的成员:
1. 线程表 -- vector<ThreadInfo> threads_
2. 任务表 -- queue<T> tasks_
3. 最大任务数 -- maxcap_
4. 互斥锁和条件变量 -- mutex_, p_cond_, c_cond_
二、线程池工作流程
    创建一批线程,这批线程循环、竞争、搜查任务表,有任务工作,无任务阻塞



*/
struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

template <class T>
class ThreadPool
{
private:
    void lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void psleep()
    {
        pthread_cond_wait(&p_cond_, &mutex_);
    }
    void csleep()
    {
        pthread_cond_wait(&c_cond_, &mutex_);
    }
    void pwake()
    {
        pthread_cond_signal(&p_cond_);
    }
    void cwake()
    {
        pthread_cond_signal(&c_cond_);
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (auto &e : threads_)
        {
            if (e.tid == tid)
            {
                return e.name;
            }
        }
        return "wu";
    }

private:
    ThreadPool(int threadsnum = 5, int maxcap = 10)
        : maxcap_(maxcap), threads_(threadsnum)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);

        for (int i = 0; i < threads_.size(); i++)
        {
            threads_[i].name = "thread-" + std::to_string(i);
            pthread_create(&(threads_[i].tid), nullptr, SolveTask, (void *)this);
        }
    }
    ThreadPool(const ThreadPool<T> &e) = delete;
    const ThreadPool &operator=(const ThreadPool &e) = delete;
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }

public:
    static void *SolveTask(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool *tp = static_cast<ThreadPool *>(args);
        while (true)
        {
            tp->lock();
            while (tp->tasks_.empty())
            {
                tp->csleep();
            }
            T out = tp->tasks_.front();
            tp->tasks_.pop();
            tp->pwake();
            tp->unlock();

            // 处理任务
            out.run();
            //
        }
    }

    void push(const T &in)
    {
        lock();
        while (tasks_.size() == maxcap_)
        {
            psleep();
        }
        tasks_.push(in);
        cwake();
        unlock();
    }

    static ThreadPool<T> *GetInstance()
    {
        
        if (nullptr == tp_)
        {
            pthread_mutex_lock(&lock_);  
            if (tp_ == nullptr)
            {
                tp_ = new ThreadPool;
                return tp_;
            }
            pthread_mutex_unlock(&lock_);
        }
        return tp_;
    }

private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;
    int maxcap_;

    pthread_mutex_t mutex_;
    pthread_cond_t c_cond_;
    pthread_cond_t p_cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>

class TcpTask
{
public:
    TcpTask(int sockfd, std::string ip, uint16_t port)
        : sockfd_(sockfd), ip_(ip), port_(port)
    {
    }
    ~TcpTask()
    {
    }
    void run()
    {
        char buffer[1024];
        ssize_t n = read(sockfd_, buffer, sizeof(buffer));
        if (n > 0)
        {
            sfw_log(Info, "server receive a message");
            buffer[n] = 0;
            std::string mes;
            mes += "server receive: ";
            mes += buffer;
            n = write(sockfd_, mes.c_str(), mes.size());
            if(n < 0)
            {
                //sfw_log(Warning, "server write fail");
            }
        }
        else if (n == 0)
        {
            //sfw_log(Info, "client quie");
        }
        else
        {
            //sfw_log(Fatal, "read error, client_ip: %s, client_port: %d", ip_.c_str(), port_);
        }
        close(sockfd_);
    }
private:
        int sockfd_;
        std::string ip_;
        uint16_t port_;
};

Tcp客户端

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

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

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

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        int cnt = 5;
        int isreconnect = false;
        int sockfd = 0;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }
        do
        {
            // 客户端发起connect的时候,系统自动bind
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                sleep(2);
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)
        {
        //连接超时
            std::cerr << "user offline..." << std::endl;
            break;
        }

        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
        if(message.size() == 0) continue;
        
        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            std::cerr << "write error..." << std::endl;
            continue;
        }

        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "server quit……" << std::endl;
        }
        close(sockfd);
    }

    return 0;
}

connect函数

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen)

守护进程

一般服务器开启后,便不会随便停止。因此我们可以把服务器进程转换为守护进程。

守护进程(Daemon Process)是在计算机操作系统中以服务方式运行的后台进程。它们通常在系统启动时启动,并在系统关闭时终止,而不与任何用户交互。守护进程通常在后台默默地执行任务,如监控、维护系统状态、处理特定的请求等

#pragma once

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
    // 1. 忽略常见的信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    // 2. 变成独立会话 --- 组长(一般是第一个进程)不能变成独立会话,因此要先变成子进程(变为第二个进程)
    if (fork() > 0)
        exit(0);
    setsid();

    // 3. 更改当前调用进程的目录
    if (!cwd.empty())
        chdir(cwd.c_str());

    // 4. 关闭012, /dev/null 垃圾桶
    int fd = open(nullfile.c_str(), O_RDWR);
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}

调用一下Daemon函数即可。

当然linux也提供了系统接口daemon, 不过一般都是自己手写。
在这里插入图片描述

  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值