【linux】TCP编程{三次握手/四次挥手/API注意点/代码}

文章目录

  • 1.API介绍
    • 1.1wc -l dirName
    • 1.2inet_pton
    • 1.3inet_aton
    • 1.4inet_ntop
  • 2.三次握手与四次挥手
    • 1.三次握手
    • 2.四次挥手
    • 3.应用程序和TCP协议层如何交互
      • 总结
  • 3.TCP 和 UDP 对比
    • 1.宏观
    • 2.详细
  • 4.地址转换函数
    • inet_ntoa
  • 5.TCP编程代码
    • Makefile
    • tcp_client.cc
    • tcp_server.cc
    • tcp_server.hpp
    • 线程池
      • Log.hpp
      • LockGuard.hpp
      • singleThreadPool.hpp
      • Task.hpp
      • thread.hpp

1.API介绍

1.1wc -l dirName

wc -l *
wc -l ./dirName/*

在这里插入图片描述

1.2inet_pton

int inet_pton(int af, const char *src, void *dst);

inet_pton 是一个在 Linux 系统和许多其他类 Unix 系统中用于处理网络地址转换的函数。它可以将点分十进制的 IP 地址(IPv4)或冒分十六进制的 IP 地址(IPv6)转换为二进制格式。这个函数在处理网络编程时非常有用,因为它允许你轻松地将人类可读的 IP 地址转换为程序可以使用的格式。

函数参数
int af: 地址族。它定义了目标地址的格式。通常,它可以是以下值之一:

AF_INET: 表示目标地址是 IPv4 地址。
AF_INET6: 表示目标地址是 IPv6 地址。
*const char src: 指向包含要转换的 IP 地址的字符串的指针。这个字符串应该是一个以空字符结尾的字符串,其格式取决于 af 参数的值。

*void dst: 指向存储转换后的二进制地址的缓冲区的指针。这个缓冲区的大小应该足够大,以容纳转换后的地址。对于 IPv4,缓冲区至少应该为 4 字节;对于 IPv6,缓冲区至少应该为 16 字节。

函数功能
inet_pton 函数的功能是将点分十进制或冒分十六进制的 IP 地址字符串转换为相应的二进制格式,并将结果存储在提供的目标缓冲区中。

函数工作原理
函数的工作原理大致如下:

根据 af 参数的值确定目标地址的格式(IPv4 或 IPv6)。
解析 src 指向的字符串,将其从人类可读的格式转换为二进制格式。
将转换后的二进制地址存储在 dst 指向的缓冲区中。
返回值
inet_pton 函数的返回值表示转换是否成功:

如果转换成功,函数返回 1。
如果输入的字符串不是有效的 IP 地址,函数返回 0。
如果发生错误(例如,无效的 af 参数或 dst 缓冲区太小),函数返回 -1。
示例
下面是一个简单的示例,展示如何使用 inet_pton 函数将 IPv4 地址字符串转换为二进制格式:

c
#include <stdio.h>  
#include <arpa/inet.h>  
  
int main() {  
    const char *ip_str = "192.168.1.1";  
    struct in_addr ip_bin;//in_addr: struct sockaddr_in 里用于存储ip的结构体字段
  
    if (inet_pton(AF_INET, ip_str, &ip_bin) == 1) {  
        printf("Binary IP address: %u\n", ip_bin.s_addr);  
    } else {  
        printf("Invalid IP address or error occurred.\n");  
    }  
  
    return 0;  
}

在这个示例中,我们将字符串 “192.168.1.1” 转换为二进制格式,并打印结果。注意,我们使用了 struct in_addr 结构体来存储 IPv4 地址的二进制表示。

1.3inet_aton

int inet_aton(const char *cp, struct in_addr *inp);

inet_aton 是 Linux 系统和其他类 Unix 系统中用于将点分十进制的 IPv4 地址转换为网络字节序的二进制形式的函数。这个函数在处理网络编程时非常有用,因为它允许开发者将人类可读的 IP 地址字符串转换为计算机可以直接使用的格式。

函数参数
*const char cp: 指向包含要转换的 IPv4 地址字符串的指针。这个字符串应该是一个以空字符结尾的字符串,并且应该符合点分十进制的格式,例如 “192.168.1.1”。
*struct in_addr inp: 指向存储转换后的二进制地址的 struct in_addr 结构体的指针。这个结构体用于存储 IPv4 地址的二进制表示。
函数功能
inet_aton 函数的功能是将点分十进制的 IPv4 地址字符串转换为网络字节序的二进制形式,并将结果存储在提供的 struct in_addr 结构体中。

函数工作原理
函数的工作原理大致如下:

解析 cp 指向的字符串,检查它是否符合点分十进制的 IPv4 地址格式。
如果字符串是有效的 IPv4 地址,函数将其转换为网络字节序的二进制形式。网络字节序是大端字节序,即高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
将转换后的二进制地址存储在 inp 指向的 struct in_addr 结构体中。
返回值
inet_aton 函数的返回值是一个整型值,用于表示转换是否成功:

如果转换成功,函数返回非零值(通常是 1)。
如果输入的字符串不是有效的 IPv4 地址,函数返回零。
示例
下面是一个简单的示例,展示如何使用 inet_aton 函数将 IPv4 地址字符串转换为二进制格式:

c
#include <stdio.h>  
#include <arpa/inet.h>  
#include <string.h>  
  
int main() {  
    const char *ip_str = "192.168.1.1";  
    struct in_addr ip_bin;  
  
    if (inet_aton(ip_str, &ip_bin)) {  
        unsigned long ip_num = ip_bin.s_addr; // 获取网络字节序的IP地址  
        printf("Binary IP address (in network byte order): %lu\n", ip_num);  
    } else {  
        printf("Invalid IP address.\n");  
    }  
  
    return 0;  
}

在这个示例中,我们将字符串 “192.168.1.1” 转换为网络字节序的二进制格式,并打印结果。注意,struct in_addr 结构体中的 s_addr 成员用于存储网络字节序的 IPv4 地址。如果需要将其转换为主机字节序(即小端字节序),可以使用 ntohl 函数。

1.4inet_ntop

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

inet_ntop 是 Linux 和其他类 Unix 系统中用于将网络地址从数值格式转换为人类可读的字符串格式的函数。它是 inet_pton 的反向操作,用于将二进制格式的 IP 地址转换回点分十进制(IPv4)或冒分十六进制(IPv6)字符串表示。

函数参数
int af: 地址族。它定义了源地址的格式。常用的值有:

AF_INET: 表示源地址是 IPv4 地址。
AF_INET6: 表示源地址是 IPv6 地址。
*const void src: 指向包含要转换的二进制地址的缓冲区的指针。这个缓冲区应该包含由 inet_pton 或其他方法设置的有效的二进制地址。

*char dst: 指向存储转换后的字符串的缓冲区的指针。这个缓冲区应该足够大,以容纳转换后的字符串,包括末尾的空字符。对于 IPv4 地址,缓冲区大小至少应为 INET_ADDRSTRLEN(通常为 16 字节);对于 IPv6 地址,缓冲区大小至少应为 INET6_ADDRSTRLEN(通常为 46 字节)。

socklen_t size: 指定 dst 缓冲区的大小。这有助于防止缓冲区溢出。

函数功能
inet_ntop 的功能是将二进制格式的 IP 地址转换为点分十进制(IPv4)或冒分十六进制(IPv6)的字符串表示。

函数工作原理
函数的工作原理大致如下:

根据 af 参数的值确定源地址的格式(IPv4 或 IPv6)。
解析 src 指向的二进制缓冲区,将其从二进制格式转换为人类可读的字符串格式。
将转换后的字符串存储在 dst 指向的缓冲区中,并在字符串末尾添加一个空字符。
如果 dst 缓冲区太小,无法容纳完整的字符串(包括末尾的空字符),则函数可能无法正常工作,或者结果可能被截断。
返回值
inet_ntop 函数的返回值是一个指向转换后的字符串的指针。如果转换成功,该指针将指向 dst 缓冲区中的字符串。如果发生错误(例如,无效的 af 参数或 dst 缓冲区太小),函数将返回 NULL。

示例
下面是一个简单的示例,展示如何使用 inet_ntop 函数将 IPv4 地址的二进制表示转换为点分十进制字符串:

c
#include <stdio.h>  
#include <arpa/inet.h>  
#include <string.h>  
  
int main() {  
    struct in_addr ip_bin = { .s_addr = 0x01020304 }; // 示例的 IPv4 地址(二进制格式)  
    char ip_str[INET_ADDRSTRLEN]; // 用于存储转换后的字符串的缓冲区  
  
    const char *result = inet_ntop(AF_INET, &ip_bin, ip_str, sizeof(ip_str));  
    if (result != NULL) {  
        printf("IP address in human-readable format: %s\n", ip_str);  
    } else {  
        perror("inet_ntop failed");  
        return 1;  
    }  
  
    return 0;  
}

在这个示例中,我们创建了一个包含示例 IPv4 地址的 struct in_addr 结构体,并使用 inet_ntop 将其转换为点分十进制的字符串表示。然后,我们打印出转换后的字符串。如果 inet_ntop 函数失败,我们会打印一个错误消息。

2.三次握手与四次挥手

在这里插入图片描述

1.三次握手

服务器初始化

  1. 调用socket, 创建文件描述符;
  2. 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  3. 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  4. 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为 三次握手;

2.四次挥手

数据传输的过程

  1. 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  2. 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  3. 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  4. 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  5. 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

  1. 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  2. 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  3. read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  4. 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

这个断开连接的过程, 通常称为 四次挥手

3.应用程序和TCP协议层如何交互

  1. 应用程序调用某个socket函数时,TCP协议层会完成什么动作:比如调用connect()会发出SYN段
  2. 应用程序如何知道TCP协议层的状态变化,比如:从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

总结

TCP的三次握手与四次挥手是TCP/IP协议中用于建立和维护可靠传输连接的重要过程。

三次握手:

第一次握手:客户端向服务器发送一个SYN包,并等待服务器确认。此时,客户端进入SYN_SENT状态。
第二次握手:服务器收到SYN包后,向客户端发送一个SYN/ACK包,确认客户端的SYN包,并请求客户端确认。此时,服务器进入SYN_RECV状态。
第三次握手:客户端收到SYN/ACK包后,向服务器发送一个ACK包,表示已完成连接。此时,客户端和服务器都进入ESTABLISHED状态,完成TCP三次握手。
三次握手的主要目的是确认双方的通信能力和理解能力,确保双方的初始序列号一致,以便后续的可靠传输。通过三次握手,TCP连接得以建立,数据可以开始传输。

四次挥手:

四次挥手主要用于终止一个已经建立的TCP连接。这个过程由TCP的半关闭特性决定,即TCP连接的一端在结束发送后还能接收来自另一端的数据。

第一次挥手:客户端发送一个FIN包给服务器,请求关闭连接。此时,客户端进入FIN_WAIT_1状态。
第二次挥手:服务器收到FIN包后,发送一个ACK包给客户端,确认收到关闭请求。此时,服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
第三次挥手:服务器在关闭连接前发送一个FIN包给客户端,请求关闭连接。此时,服务器进入LAST_ACK状态。
第四次挥手:客户端收到FIN包后,发送一个ACK包给服务器,确认收到关闭请求。此时,客户端进入TIME_WAIT状态,等待一段时间后进入CLOSED状态,服务器收到ACK包后也进入CLOSED状态,完成TCP四次挥手。
四次挥手的主要目的是确保在关闭连接前,双方都能完成数据的传输和确认,避免数据丢失或重复传输。

总的来说,TCP的三次握手和四次挥手是TCP协议中用于建立和维护可靠传输连接的重要机制,它们确保了数据的可靠传输和完整性。

3.TCP 和 UDP 对比

1.宏观

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

2.详细

  1. 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接;
  2. 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦;
  3. 客户端需要调用connect()连接服务器;connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址; connect()成功返回0,出错返回-1;
  4. addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
  5. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;

4.地址转换函数

基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_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_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr

inet_ntoa

在这里插入图片描述

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

如果我们调用多次这个函数, 会有什么样的效果呢?为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

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

/*
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};
struct sockaddr_in
{
    in_port_t sin_port;
    struct in_addr sin_addr;
};
*/

// in_addr转字符串
int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    char *ptr1 = inet_ntoa(addr1.sin_addr);
    char *ptr2 = inet_ntoa(addr2.sin_addr);

    printf("ptr1:%s, ptr2:%s\n", ptr1, ptr2);
    return 0;
}
  1. 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  2. 在APUE(Advanced Programming in the UNIX Environment[一本书])中, 明确提出inet_ntoa不是线程安全的函数;
  3. 在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  4. 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题.

centos7 测试

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    int count = 5;
    while (count--)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }
    return NULL;
}
void *Func2(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    int count = 5;

    while (count--)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}
int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    pthread_t tid1 = 0;
    pthread_create(&tid1, NULL, Func1, &addr1);

    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

在这里插入图片描述

5.TCP编程代码

Makefile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11 
tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11 -lpthread -DDEBUG_COMPILE

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

tcp_client.cc

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

static void usage(std::string proc)
{
    std::cout << std::endl
              << "Usage: " << proc << " port" << std::endl
              << std::endl;
}

//服务端的字典服务实现的是短服务 即为一个客户端服务一次后 该服务停止 客户端再次申请服务时必须再次socket/connect

// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);

    volatile bool isAlive = false;
    int socketFd = 0;
    std::string line;
    char buf[1024];

    while (true)
    {
        if (isAlive == false)
        {
            socketFd = socket(AF_INET, SOCK_STREAM, 0);
            if (socketFd < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }

            // client 不需要显示的bind 他肯定是需要port
            // client的port让os自动选择 从而有连接别人的能力
            struct sockaddr_in dst_sockAddr;
            memset(&dst_sockAddr, 0, sizeof(dst_sockAddr));
            dst_sockAddr.sin_family = AF_INET;
            dst_sockAddr.sin_addr.s_addr = inet_addr(serverIp.c_str());
            dst_sockAddr.sin_port = htons(serverPort);

            if (connect(socketFd, (struct sockaddr *)&dst_sockAddr, sizeof(dst_sockAddr)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3);
            }
            std::cout << "connect success" << std::endl;
            isAlive = true;
        }

        std::cout << "请输入# ";
        std::cin >> std::ws; // 跳过前导空白字符
        std::getline(std::cin, line);
        if (line == "quit")
        {
            close(socketFd);
            isAlive = false;
            break;
        }

        ssize_t sendBytes = send(socketFd, line.c_str(), line.size(), 0);
        sleep(1); // 等待缓冲区中的数据发送过去
        if (sendBytes < 0)
        {
            std::cerr << "send error!" << std::endl;

            close(socketFd);
            isAlive = false;

            continue;
        }
        memset(buf, 0, sizeof buf);
        ssize_t recvBytes = recv(socketFd, buf, sizeof(buf) - 1, 0); // typedef ssize_t long int
        if (recvBytes > 0)
        {
            buf[recvBytes] = 0;
            std::cout << "server echo# " << buf << std::endl;
        }
        else
        {
            std::cerr << "recv error!" << std::endl;

            close(socketFd);
            isAlive = false;
        }
    }

    return 0;
}

tcp_server.cc

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

static void usage(std::string proc)
{
    std::cout << std::endl
              << "Usage: " << proc << " port" << std::endl
              << std::endl;
}

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

    std::unique_ptr<TcpServer> unqPtrSvr(new TcpServer(port));

    unqPtrSvr->initServer();
    unqPtrSvr->start();

    return 0;
}

tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cctype>
#include <memory>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 1.头文件引用 如果是.h和.c分开写 Makefile里要修改
// 2.打包成静态库/动态库使用
// 3.头文件 路径引用 .hpp
#include "singleThreadPool/Log.hpp"
#include "singleThreadPool/singleThreadPool.hpp"
#include "singleThreadPool/Task.hpp"

static void echoService(int serviceSocket, const std::string &clientip, const uint16_t &clientport)
{
    char buf[1024];
    while (true)
    {
        // 在服务端显示客户端发送的信息
        ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
        if (s > 0)
        {
            buf[s] = 0; // 将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buf << std::endl;
        }
        else if (s == 0) // 对方停止写入
        {
            logMsg(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
            break;
        }

        // 把客户端发的信息再传回去
        write(serviceSocket, buf, strlen(buf));
    }
    close(serviceSocket);
}

// 当前我们的服务器是一旦有客户端申请服务 服务端连接上后 只要客户端不退出 服务端就一直进行服务如回显他的消息
// 引进线程池处理任务后 对于这种长连接的服务 如果同时有超过g_threadNum个客户端申请服务
// 线程池里的线程就不够用了 客户端发来的服务请求即任务就会先放到队列里 知道有客户端服务结束退出
// 知识:线程数量要有上限 否则如果同时有大量客户申请服务 此时OS中的线程数量就会剧增 效率降低是一方面
// OS为了为新的客户提供服务 可能会停止先前的进程 这对OS是不好的
// 解决:
// 1. 参考OS设计的进程/线程调度问题 给一个客户服务长达一定时间 就停止对他服务 转向服务别人 一段时间后再来对他服务
// 2.如果就是想要长期进行服务 这个问题后续再讲解
static void Task_echo(int serviceSocket, const std::string &clientip,
                      const uint16_t &clientport, const std::string &threadName)
{
    char buf[1024];
    while (true)
    {
        // 在服务端显示客户端发送的信息
        ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
        if (s > 0)
        {
            buf[s] = 0; // 将发过来的数据当做字符串
            std::cout << threadName << "|" << clientip << ":" << clientport << " client send# " << buf << std::endl;
        }
        else if (s == 0) // 对方停止写入或关闭连接
        {
            logMsg(NORMAL, "%s:%d client shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
            break;
        }

        // 把客户端发的信息再传回去
        write(serviceSocket, buf, strlen(buf));
    }
    close(serviceSocket);
}

// 大小写转换
static void Task_toggle(int serviceSocket, const std::string &clientip,
                        const uint16_t &clientport, const std::string &thread_name)
{
    char buf[1024];
    ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
    if (s > 0)
    {
        buf[s] = 0; // 发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << " client send# " << buf << std::endl;
        std::string msg;
        char *start = buf;
        while (*start)
        {
            char character;
            if (islower(*start))
                character = toupper(*start);
            else
                character = *start;
            msg.push_back(character);
            start++;
        }

        write(serviceSocket, msg.c_str(), msg.size());
    }
    else if (s == 0) // 对方停止写入或关闭连接
    {
        logMsg(NORMAL, "%s:%d client shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    {
        logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
    }

    close(serviceSocket);
}
static void Task_Dictionary(int serviceSocket, const std::string &clientip,
                            const uint16_t &clientport, const std::string &thread_name)
{
    static std::unordered_map<std::string, std::string> dictionary =
        {
            {"apple", "苹果"},
            {"banana", "香蕉"},
            {"hard", "好难啊"}};

    char EnglishBuf[1024];
    ssize_t readBytes = read(serviceSocket, EnglishBuf, sizeof(EnglishBuf) - 1);
    if (readBytes > 0)
    {
        EnglishBuf[readBytes] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "|"
                  << " client send# " << EnglishBuf << std::endl;

        std::string msg;
        auto iter = dictionary.find(EnglishBuf);
        if (iter == dictionary.end())
            msg = "unknown";
        else
            msg = iter->second;

        write(serviceSocket, msg.c_str(), msg.size());
    }
    else if (readBytes == 0) // 对方停止写入或关闭连接
    {
        logMsg(NORMAL, "%s:%d client shutdown!", clientip.c_str(), clientport);
    }
    else
    {
        logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
    }

    close(serviceSocket);
}

/*// version 3多线程
class ThreadInfo
{
public:
    int _socketFd;
    std::string _ip;
    uint16_t _port;
};
*/

class TcpServer
{
private:
    const static int g_backlog = 20;

    /*// version 3多线程
    //类内成员函数做回调函数得是静态函数 -- this
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 自动释放资源
       
        ThreadInfo *threadinfo = static_cast<ThreadInfo *>(args);
        echoService(threadinfo->_socketFd, threadinfo->_ip, threadinfo->_port);

        delete threadinfo;
        return nullptr;
    }
    */

public:
    TcpServer(uint16_t port, std::string ip = "127.0.0.1")
        : _listenSocket(-1),
          _port(port),
          _ip(ip),
          _ptrToThreadPool(singleThreadPool<Task>::getThreadPoolInstance())
    {
    }

    void initServer()
    {
        // 1. 创建socket -- 进程+文件
        _listenSocket = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSocket < 0)
        {
            logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMsg(NORMAL, "show listenSocket: %d", _listenSocket); // fd = 3

        // 2. bind -- 文件+网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);

        local.sin_family = AF_INET;
        // inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
        // inet_aton(_ip.c_str(), &local.sin_addr);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        local.sin_port = htons(_port);

        if (bind(_listenSocket, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. TCP面向连接 当正式通信时 要先建立连接
        if (listen(_listenSocket, g_backlog) < 0)
        {
            logMsg(FATAL, "listen::%d:%s", errno, strerror(errno));
            exit(4);
        }

        logMsg(NORMAL, "init server success");
    }

    void start()
    {
        // Version 2.0 主动忽略SIGCHLD信号 子进程退出时自动释放自己的资源
        // signal(SIGCHLD, SIG_IGN);

        _ptrToThreadPool->run();
        while (true)
        {
            // sleep(1);

            // 4. 接收连接accept
            struct sockaddr_in src_sockAddr;
            socklen_t len = sizeof(src_sockAddr);
            int serviceSocket = accept(_listenSocket, (struct sockaddr *)&src_sockAddr, &len);
            if (serviceSocket < 0)
            {
                logMsg(ERROR, "accept::%d:%s", errno, strerror(errno));
                continue;
            }

            // 接收连接成功
            uint16_t client_port = ntohs(src_sockAddr.sin_port);
            std::string client_ip = inet_ntoa(src_sockAddr.sin_addr);
            logMsg(NORMAL, "link success, serviceSocket: %d | %s : %d |\n",
                   serviceSocket, client_ip.c_str(), client_port);

            /* // Version 1单进程回显服务器
            // 一次处理一个客户端 处理完一个再处理下一个
            // 多个客户端给服务端发消息 服务端只能接收到第一个客户端的信息
            // 因为一旦第一个客户端连接上服务端后 服务端就就调用echoService()
            // 该函数是一个死循环 他在不断地读取回显客户端发送的数据 无法再与新的客户端建立连接 除非有客户端退出
            echoService(serviceSocket, client_ip, client_port);
            close(serviceSocket);
            */

            /*// Version 2.0多进程 信号处理子进程问题
            // 创建子进程 让子进程给新的连接提供服务 子进程能打开父进程曾经打开的文件
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                // 子进程是来进行提供服务的 不需要知道listenSocket
                //关闭不需要的套接字 防止非法访问
                close(_listenSocket);

                echoService(serviceSocket, client_ip, client_port);
                exit(0);
            }

            // 父进程: 每一次accept一个客户端 都会创建一个serviceSocket
            // 如果在该层循环即将结束时 不关闭该层的serviceSocket fd资源会越来越少
            // 此处关闭serviceSocket 对父进程无影响 因为欸父进程只需要监听套接字 不需要提供服务的套接字
            // fd是一种有限资源【数组下标】有的内核是32/64 云服务器一般是5-10w
            // 服务器死循环 任何一种导致资源浪费的情况 都是服务器编程要考虑的
            close(serviceSocket); // 父进程关闭serviceSocket不会影响子进程

            // waitpid();
            // 多进程版初衷是让子进程为新客户端服务 此时父进程又来等待子进程的退出
            // 如果不等待子进程先于父进程退出会进入僵尸状态 如果等待又违反了多进程的初衷
            // ==>1.非阻塞式等待 但是较麻烦 OS会记录下子进程的pid 然后不断轮询 2.SIGCHILD
            */

            /*// version2.1多进程 创建孤儿进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                close(_listenSocket);
                if (fork() > 0)
                    exit(0); // 子进程退出

                // 孙子进程:孤儿进程退出时 OS自动回收
                // 子进程已退出 孙进程的父进程即["子进程"]先于自己退出 孙进程成为孤儿进程
                echoService(serviceSocket, client_ip, client_port);
                exit(0);
            }
            // 父进程
            waitpid(id, nullptr, 0); // 子进程诞生即退出 无需阻塞等待
            close(serviceSocket);
            */

            /*// version 3多线程
            // 在栈上创建ThreadInfo对象 把该对象的地址传给pthread_create
            // threadRoutine去访问该对象 接着主线程就会进行下一次循环
            // 该对象被生命周期结束 但是threadRoutine可能还未结束 -- 悬空指针
            ThreadInfo *threadinfo = new ThreadInfo();
            threadinfo->_socketFd = serviceSocket;
            threadinfo->_ip = client_ip;
            threadinfo->_port = client_port;
            pthread_t tid;

            // 多线程与主线程共享fd 当本次通信结束即对应的客户端退出 再关闭serviceSocket
            pthread_create(&tid, nullptr, threadRoutine, threadinfo);
            */

            //  verison 4线程池
            //此处为测试:默认知道客户端请求的是什么任务
            //Task taskEcho(serviceSocket, client_ip, client_port, Task_echo);
            //Task taskToggle(serviceSocket, client_ip, client_port, Task_toggle);
            Task taskDictionary(serviceSocket, client_ip, client_port, Task_Dictionary);

            _ptrToThreadPool->pushTask(taskDictionary);
        }
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    std::string _ip;
    int _listenSocket;

    std::unique_ptr<singleThreadPool<Task>> _ptrToThreadPool;
};

线程池

Log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

static const  char *gLevelMap[] = {
    " DEBUG ",
    " NORMAL",
    "WARNING",
    " ERROR ",
    " FATAL "};

#define LOGFILE "./singleThreadPool.log"

// 日志功能: 日志等级 时间 用户自定义(日志内容/文件名/文件行) 等
void logMsg(int level, const char *format, ...)
{
#ifndef DEBUG_COMPILE // 非调试编译下 不输出DEBUG信息
    if (level == DEBUG)
        return;
#endif

    // 1.标准日志内容
    char stdBuf[1024];
    // 1.1获取时间戳
    time_t timestamp = time(nullptr);
    if (timestamp == std::time_t(-1))
    {
        std::cerr << "获取时间失败" << std::endl;
        exit(1);
    }
    // 1.2获取格式化时间
    struct tm *CLK = std::localtime(&timestamp); // tm *localtime(const time_t *__timer)
    // 1.3将日志信息输出到日志文件
    //  snprintf(stdBuf, sizeof stdBuf, "[%s] [%ld] ", gLevelMap[level], timestamp);
    snprintf(stdBuf, sizeof stdBuf, "[%s] [%d/%d/%d %d:%d:%d ", gLevelMap[level],
             1900 + CLK->tm_year, 1 + CLK->tm_mon, CLK->tm_mday, CLK->tm_hour, CLK->tm_min, CLK->tm_sec);

    // 2.用户自定义内容
    va_list args;
    va_start(args, format);//args指向可变参数列表起始位置
    char logBuf[1024];
    // int vsnprintf(char *str, size_t size, const char *format, va_list ap);
    vsnprintf(logBuf, sizeof logBuf, format, args);
    va_end(args); //释放资源 回复状态

    //向标准输出打印
    fprintf(stdout, "%s%s\n", stdBuf, logBuf);

    //向文件中打印
    // FILE *fp = fopen(LOGFILE, "a");
    // fprintf(fp, "%s%s\n", stdBuf, logBuf);
    // fclose(fp);
}

LockGuard.hpp

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx)
        : _pmtx(mtx)
    {
    }

    void lock()
    {
        //std::cout << "加锁中..." << std::endl;
        pthread_mutex_lock(_pmtx);
    }

    void unlock()
    {
        //std::cout << "解锁中..." << std::endl;
        pthread_mutex_unlock(_pmtx);
    }

    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_pmtx;
};

// RAII风格的加锁方式
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx)
        : _mtx(mtx)
    {
        _mtx.lock();
    }

    ~lockGuard()
    {
        _mtx.unlock();
    }

private:
    Mutex _mtx;
};

singleThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "Log.hpp"

const int g_threadNum = 10;

template <class T>
class singleThreadPool
{
private:
    // 构造函数
    // 线程池创造多个线程(把每个线程的启动例程函数 及 创建这个线程的线程池指针 传过去)
    singleThreadPool(int thread_num = g_threadNum)
        : _threadNum(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);

        for (int i = 1; i <= _threadNum; i++)
        {
            // 初始化列表区域 对象还未存在 走到函数块{}内 对象已存在 可以使用this指针
            _threads.push_back(new Thread(i, startRoutine, this));
        }
    }
    singleThreadPool(const singleThreadPool<T> &copy) = delete;
    const singleThreadPool<T> &operator=(const singleThreadPool<T> &copy) = delete;

public:
    // 考虑 多线程使用单例 的情况
    static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
    {
        if (nullptr == ptrThreadPool)
        {
            lockGuard lockguard(&mutex_single);
            if (nullptr == ptrThreadPool)
            {
                ptrThreadPool = new singleThreadPool<T>(threadNum);
            }
        }
        return ptrThreadPool;
    }
    
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }

    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }

    bool isEmpty()
    {
        return _taskQueue.empty();
    }

    void pushTask(const T &task)
    {
        lockGuard lockguard(&lock);
        _taskQueue.push(task);
        pthread_cond_signal(&cond);
    }

    T getTask()
    {
        T t = _taskQueue.front();
        _taskQueue.pop();
        return t;
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            // std::cout << iter->name() << " 启动成功" << std::endl;
            logMsg(NORMAL, "%s %s", iter->getThreadName().c_str(), "启动成功");
        }
    }

    static void *startRoutine(void *args)
    {
        ThreadInfo *threadinfo = (ThreadInfo *)args;
        singleThreadPool<T> *tp = (singleThreadPool<T> *)threadinfo->_ptrThreadPool;
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                    tp->waitCond();
                task = tp->getTask();
            }
            task(threadinfo->_threadName);
        }
    }

    ~singleThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

private:
    int _threadNum;
    std::vector<Thread *> _threads;
    std::queue<T> _taskQueue;
    pthread_mutex_t lock;
    pthread_cond_t cond;

    static singleThreadPool<T> *ptrThreadPool;
    static pthread_mutex_t mutex_single;
};

// 类内声明 类外初始化
template <typename T>
singleThreadPool<T> *singleThreadPool<T>::ptrThreadPool = nullptr;

template <typename T>
pthread_mutex_t singleThreadPool<T>::mutex_single = PTHREAD_MUTEX_INITIALIZER;

Task.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"

// typedef std::function<void (int, const std::string &, const uint16_t &, const std::string &)> func_t;
using func_t = std::function<void(int, const std::string &, const uint16_t &, const std::string &)>;

class Task
{
public:
    Task() {}
    Task(int sock, const std::string ip, uint16_t port, func_t startRoutine)
        : _socketFd(sock),
          _ip(ip),
          _port(port),
          _startRoutine(startRoutine)
    {
    }

    void operator()(const std::string &threadName)
    {
        _startRoutine(_socketFd, _ip, _port, threadName);
        // logMsg(NORMAL, "%s running | %s | %d | %s | %s", threadName.c_str(), __FILE__, __LINE__, __DATE__, __TIME__);
    }

public:
    int _socketFd;
    std::string _ip;
    uint16_t _port;
    func_t _startRoutine;
};

thread.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);

class ThreadInfo
{
public:
    std::string _threadName;
    void *_ptrThreadPool;
};

class Thread
{
public:
    Thread(int index, fun_t startRoutine, void *ptrTotp)
        : _startRoutine(startRoutine)
    {
        char nameBuf[64];
        snprintf(nameBuf, sizeof nameBuf, "Thread-%d", index);
        _name = nameBuf;

        _tInfo._threadName = _name;
        _tInfo._ptrThreadPool = ptrTotp;
    }

    void start()
    {
        pthread_create(&_tid, nullptr, _startRoutine, (void *)&_tInfo);
    }

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

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

    ~Thread()
    {
    }

private:
    pthread_t _tid;
    std::string _name;
    fun_t _startRoutine;
    ThreadInfo _tInfo;
};

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

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

相关文章

TypeScript学习--day1

一、介绍 TypeScript是JS的超集&#xff0c;为JS添加了类型支持。 1.1 为什么添加类型支持 JS代码的错误大部分是类型错误&#xff0c;增加改Bug时间&#xff0c;影响开发效率。 静态类型&#xff1a;编译期做类型检查 动态类型&#xff1a;执行期做类型检查 TS--静态类型编…

前端保留两位小数

一、保留两位小数&#xff08;四舍五入&#xff09; 解决方案&#xff1a;使用 toFixed(x) 方法可以对小数进行指定位数保留&#xff0c;其中x是要保留的位数用法&#xff1a;num.toFixed(x)&#xff0c;其中num为需要操作的数据&#xff0c;x为要保留的位数示例&#xff1a;1…

力扣LeetCode138. 复制带随机指针的链表 两种解法(C语言实现)

目录 题目链接 题目分析 题目定位&#xff1a; 解题思路 解题思路1&#xff08;粗暴但是复杂度高&#xff09; 解题思路2&#xff08;巧妙并且复杂度低&#xff09; 题目链接 138. 复制带随机指针的链表https://leetcode-cn.com/problems/copy-list-with-random-pointer/ …

双写一致性问题

双写一致性问题&#xff1a;同一份数据&#xff0c;需要写数据库、写缓存。数据库中的数据和缓存中的数据要一致 解决办法&#xff1a;延迟双删 当我们要进行更新操作时&#xff0c;先删除缓存&#xff0c;再更新数据库&#xff0c;延迟几百ms再删除一次redis的缓存数据。 示…

2023年蓝桥杯——日期统计

目录 题目链接&#xff1a;1.日期统计 - 蓝桥云课 (lanqiao.cn) 题目描述 思路 代码思路 定义数据结构&#xff1a; 处理每一个月&#xff1a; 检查日期序列在num100中是否存在&#xff1a; 计数匹配的日期数&#xff1a; 输出结果&#xff1a; 代码实现 总结 题目链…

【Python习题】某景区门票的优惠措施为:购买5张以内门票不打折,5到20张打九折,20张以上打八折。编写程序,根据购买的门票数量,输出总票价。

题干 某景区门票的优惠措施为:购买5张以内门票不打折&#xff0c;5到20张打九折&#xff0c;20张以上打八折。编写程序&#xff0c;根据购买的门票数量&#xff0c;输出总票价。 代码

介绍几个好用的电商(淘宝京东1688)API接口,可测试

以下是几个好用的电商&#xff08;淘宝、京东、1688&#xff09;API接口&#xff0c;这些接口都可以进行测试以确保其稳定性和可用性&#xff1a; taobao.item_get-获取淘宝商品数据接口返回值说明 1.请求方式&#xff1a;HTTP POST GET &#xff08;复制薇&#xff1a;Anzex…

2024.4.13 Python 爬虫复习day01

目录 day01_HTTP协议HTML页面web服务器 各类名词解释 URL统一资源定位符 HTTP协议 HTML页面 知识点: 第一个页面 标题标签和图片标签 注册页面 登录页面 WEB服务器 安装fastapi和uvicorn 原始命令方式 镜像源命令方式 工具方式 快速搭建web服务器 知识点: 示例…

CH254X 8051芯片手册介绍

1 8051CPU 8051是一种8位元的单芯片微控制器&#xff0c;属于MCS-51单芯片的一种&#xff0c;由英特尔(Intel)公司于1981年制造。Intel公司将MCS51的核心技术授权给了很多其它公司&#xff0c;所以有很多公司在做以8051为核心的单片机&#xff0c;如Atmel、飞利浦、深联华等公…

ARMv8-A架构下的外部debug模型之外部调试事件(external debug events)概述

外部调试器与处理器之间的握手与external debug events 一&#xff0c;External Debug的使能二&#xff0c;外部调试器和CPU之间的握手三&#xff0c;外部调试事件 External debug events1. External debug request event2. Halt instruction debug event3. Halting step debug…

是的,本科毕业八年,我考研了

今天&#xff0c;是一篇纯分享文。 是的&#xff0c;本科毕业八年&#xff0c;我考研了。 停更10个月&#xff0c;历时296天&#xff0c;我考研上岸了。 小伙伴们&#xff0c;好久不见。 一 发今年第一篇文章的时候刚处理完后续事宜&#xff0c;就简单说了句&#xff0c;后台…

Vue3 ts环境下的PropType

简介 在Typscript中&#xff0c;我们可以使用PropType进行类型的推断与验证。在日常的开发中我们常常会遇到下面这样的场景&#xff1a; 我们通过request请求从服务端获取了一条数据&#xff0c;数据是个Array的格式&#xff0c;Array中的每个元素又是一个对象&#xff0c;像下…

【神经网络与深度学习】循环神经网络基础

tokenization tokenization&#xff1a;分词 每一个词语都是token 分词方法&#xff1a;转为单个词、转为多个词语 N-gram表示法 准备词语特征的方法 &#xff08;把连续的N个词作为特征&#xff09; 如 ”我爱你“——>[我&#xff0c;爱&#xff0c;你] 2-gram——[[我…

java项目之校园兼职系统(ssm框架+mysql数据库+文档)

项目简介 校园兼职系统的主要使用者分为&#xff1a;管理员&#xff1a;首页、个人中心、专业管理、商家管理、热门兼职管理、学生管理、兼职接单管理、学生咨询管理、兼职任务管理、完成评价管理、管理员管理、系统管理等模块信息的查看及相应操作&#xff1b;学生&#xff1…

在vue中配置样式 max-width:100px时,发现和width:100px一样没有对应的递增到最大宽度的效果?怎么回事?怎么解决?

原因&#xff1a; 可能时vue的样式大部分和display相关&#xff0c;有很多的联系&#xff0c;导致不生效 解决&#xff1a; 对设置max-width样式的元素设置display:inline-block;属性&#xff0c;即可生效&#xff0c;实现随着子元素的扩展而扩展并增加固定到最大的宽度

使用 ASE 拼接分子

在部分应用场景下&#xff0c;我们需要对两个分子片段进行拼接&#xff0c;例如锂电电解液数据库 LiBE 然而&#xff0c;当前并没有合适的拼接方法。下面是一些已有方法的调研结果&#xff1a; 在 LiBE 论文的附录里&#xff0c;作者使用 pymatgen 进行分子拼接。 其思路是&…

分享2024高校专业建设思路及建设效果

广东泰迪智能科技股份有限公司成立于2013年&#xff0c;是一家专业从事大数据、人工智能等数据智能技术研发、咨询和培训的高科技企业&#xff0c;公司基于十余年的数据智能产业实践经验&#xff0c;构建“产、岗、课、赛、证、文”融通的特色应用型人才培养模式&#xff0c;助…

MQ:延迟队列

6.1场景&#xff1a; 1.定时发布文章 2.秒杀之后&#xff0c;给30分钟时间进行支付&#xff0c;如果30分钟后&#xff0c;没有支付&#xff0c;订单取消。 3.预约餐厅&#xff0c;提前半个小时发短信通知用户。 A -> 13:00 17:00 16:30 延迟时间&#xff1a; 7*30 * 60 *…

微信营销快捷回复和微信多开-微信UI自动化(.Net)

整理 | 小耕家的喵大仙 出品 | CSDN&#xff08;ID&#xff1a;lichao19897314&#xff09; Q Q | 978124155 关于项目背景和本软件的介绍 因为本人前期基于微信自动化这块编写了一些文章&#xff0c;所以最近想着将文章内容点合并后开发一款真正能帮助别人的软件&#xff0…

AI赋能档案开放审核:实战

关注我们 - 数字罗塞塔计划 - 为进一步推进档案开放审核工作提质增效&#xff0c;结合近几年的业务探索、研究及项目实践&#xff0c;形成了一套较为成熟、高效的AI辅助档案开放审核解决方案&#xff0c;即以“AI人工”的人机协同模式引领档案开放审机制创新&#xff0c;在档…