【Linux】揭开套接字编程的神秘面纱(下)

news2024/12/26 3:50:23

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉前言👈
    • 👉echo服务器👈
      • 单进程版
      • 多进程版
      • 多线程版
      • 线程池版
    • 👉深入剖析地址转换函数👈
    • 👉TCP协议通讯流程👈
    • 👉总结👈

👉前言👈

在揭开套接字编程神秘面纱(上)中,我们已经学习到了套接字编程的相关基础知识以及编写了基于 UDP 协议的 echo 服务器、指令服务器和简易版的公共聊天室等,那么我们现在就来学习基于 TCP 协议的套接字编程。

👉echo服务器👈

单进程版

TcpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

#define SIZE 1024

static void Service(int sock, const std::string& clientIP, uint16_t clientPort)
{
    // Echo Server
    char buffer[SIZE];
    while(true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s - 1] = '\0';
            std::cout << clientIP << " : " << clientPort << "#" << buffer << std::endl;
        }
        else if(s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d quit, me too!", clientIP.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(FATAL, "Read Fail, Errno:%d, Strerror:%s", errno, strerror(errno));
            break;
        }
        // 将消息发回去
        write(sock, buffer, strlen(buffer));
    }
}

class TcpServer
{
private:
    const static int backlog = 20; // 全队列长度
public:
    TcpServer(uint16_t port, std::string ip = "")
        : _port(port)
        , _ip(ip)
        , _listenSock(-1)
    {}

    void InitServer()
    {
        // 1. 创建套接字:SOCK_STREAM面向字节流
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listenSock < 0)
        {
            logMessage(FATAL, "Create Socket Fail! Errno:%d Strerror:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "Create Socket Success! _sock:%d", _listenSock);

        // 2. 绑定套接字
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        local.sin_port = htons(_port);
        // 绑定套接字失败
        if(bind(_listenSock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "Bind Socket Fail! Errno:%d Strerror:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,那么正式进行网络通信时,先需要建立连接
        if(listen(_listenSock, backlog) < 0)
        {
            logMessage(FATAL, "Listen Socket Fail! Errno:%d Strerrno:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "Init Server Success!");
    }

    void StartServer()
    {
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // accept函数的返回值是文件描述符,它用于后续的网络通信
            // 而_sock只用于获取新连接,并不用于后续的网络通信
            // 注:accept是阻塞等待新连接的到来
            int serviceSock = accept(_listenSock, (struct sockaddr*)&src, &len);
            if(serviceSock < 0)
            {
                logMessage(ERROR, "Accept FAIL, Errno:%d Strerrno:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功,开始进行网络通信
            uint16_t clientPort = ntohs(src.sin_port);
            std::string clientIP = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Link Success! serviceSock:%d clientIP:%s clientPort:%d", serviceSock, clientIP.c_str(), clientPort);
            // 单进程循环版
            Service(serviceSock, clientIP, clientPort);
            close(serviceSock);
        }
    }

    ~TcpServer()
    {
        if(_listenSock >= 0)
            close(_listenSock);
    }

private:
    uint16_t _port;
    std::string _ip;
    int _listenSock;
};

注:日志组件的代码在揭开套接字编程的神秘面纱(上)一文中可以找到!

listen 函数的详细介绍

int listen(int sockfd, int backlog);
  • listen 是一个用于在服务器端等待客户端连接的函数。
  • listen 函数的第一个参数 sockfd 是监听套接字(listen socket),监听套接字是一种特殊类型的套接字,用于接受连接请求,并在连接建立时创建新的套接字。监听套接字通常用于服务器程序中,服务器在特定的端口上等待客户端的连接请求。当客户端请求连接时,监听套接字会接受连接请求,并创建一个新的套接字来与客户端进行通信
  • listen 函数的第二个参数 backlog 表示服务器在接受连接请求时,最多能够排队等待的连接数。在某些情况下,服务器可能会同时收到多个客户端的连接请求,如果服务器无法及时处理这些请求,这些请求就会在队列中等待处理,此时 backlog 参数就派上用场了。
  • 具体来说,backlog 参数的值表示服务器等待连接请求的队列长度,当队列已满时,服务器会拒绝新的连接请求。如果该值过小,服务器可能无法处理所有的连接请求;如果该值过大,则会占用过多的系统资源,导致服务器性能下降。一般来说,backlog 参数的取值应该根据服务器的处理能力和网络环境等因素进行合理的设置,以确保服务器可以及时处理连接请求,同时又不会占用过多的系统资源。

查看网络状态

在这里插入图片描述

Telnet 协议

Telnet 是一种用于在互联网上进行远程登录的协议,也是一种基于文本的协议,其运行在 TCP /I P 协议上。telnet命令是一种用于测试网络连接性和调试网络问题的工具,同时也可以用于远程登录到另一个计算机。

在使用 telnet 命令时,可以通过以下语法来调用它:

telnet [选项] [主机名或IP地址] [端口号]

其中,主机名或 IP 地址指定要连接的远程主机名或 IP 地址,端口号指定要连接的远程端口。如果未指定端口号,则默认使用 23 端口(Telnet 服务端口)。

在这里插入图片描述

使用 telnet 命令时,可以先输入 telnet 命令并指定要连接的主机名和端口号。如果连接成功,将会看到远程主机上的欢迎信息。按下组合键 Ctrl + ],再按下回车键,此时就看输入信息发送给服务端了。在 telnet 会话中,可以通过输入命令来与远程主机进行交互,就像在本地终端上一样。要退出 telnet 会话,需要按下组合键 Ctrl + ],然后输入 quit 命令。

需要注意的是,由于 Telnet 协议是明文传输,不提供任何加密和安全机制,因此使用telnet进行远程登录并不安全。为了保护数据的机密性和完整性,应该使用更加安全的协议,例如 SSH(Secure Shell)协议。

在这里插入图片描述
在这里插入图片描述
单进程版的 echo 服务器的细节

因为现在服务器是单进程的,所以当有两个连接来了时,服务器只能处理一个连接,并且要当该连接关闭才能处理下一个链接!

在这里插入图片描述

TcpServer.cc

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

static void Usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " Port" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> ptr(new TcpServer(port));
    ptr->InitServer();
    ptr->StartServer();

    return 0;
}

多进程版

因为单进程版的 echo 服务器只能处理一个客户端的链接,那么我们就将其改写成多进程版。

多进程版的 TCP 服务器中,主进程(父进程)会接收客户端的连接请求,然后创建一个新的子进程来处理连接。在子进程中,会执行 TCP 通信的相关操作。当子进程处理完请求前,需要关闭不需要的文件描述符,以释放资源并确保安全性。

在多进程环境下,每个进程都有自己的文件描述符表,如果不关闭不需要的文件描述符,则可能会导致资源泄漏和安全问题。例如,一个子进程可能会在某个文件上持续进行读取操作,但是在父进程中却没有这个需要读取的文件,如果不关闭该文件描述符,则会造成资源浪费和潜在的安全问题。

因此,在多进程版的 TCP 服务器中,父进程和子进程需要各自关闭自己不需要的文件描述符,以确保每个进程都能够释放资源并保证程序的安全性。这样做可以提高程序的效率和稳定性,避免出现资源竞争和其他问题。

void StartServer()
{
    // 主动忽略SIGCHLD信号,子进程退出的时候会自动释放自己的僵尸状态
    signal(SIGCHLD, SIG_IGN); 
    while(true)
    {
        // 4. 获取连接
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        // accept函数的返回值是文件描述符,它用于后续的网络通信
        // 而_sock只用于获取新连接,并不用于后续的网络通信
        // 注:accept是阻塞等待新连接的到来
        int serviceSock = accept(_listenSock, (struct sockaddr*)&src, &len);
        if(serviceSock < 0)
        {
            logMessage(ERROR, "Accept FAIL, Errno:%d Strerrno:%s", errno, strerror(errno));
            continue;
        }
        // 获取连接成功,开始进行网络通信
        uint16_t clientPort = ntohs(src.sin_port);
        std::string clientIP = inet_ntoa(src.sin_addr);
        logMessage(NORMAL, "Link Success! serviceSock:%d clientIP:%s clientPort:%d", serviceSock, clientIP.c_str(), clientPort);
        pid_t id = fork();
        assert(id != -1);
        (void)id;
        if(id == 0)
        {
            // 子进程会继承父进程文件描述符表
            // 子进程不需要关心监听套接字
            close(_listenSock);
            Service(serviceSock, clientIP, clientPort);
            exit(0);
        }
        // 父进程不需要关系用于提供服务的套接字
        close(serviceSock);
    }
}

在这里插入图片描述

为什么多个子进程所用于通信的套接字(文件描述符)都是相等的呢?因为父进程会关闭自己所不需要的文件描述符,这个不需要的文件描述符就是 4,所以每次用于网络通信的文件描述符都是 4。

多进程的改进版

void StartServer()
{
    while(true)
    {
        // 4. 获取连接
        struct sockaddr_in src;
        socklen_t len = sizeof(src);

        int serviceSock = accept(_listenSock, (struct sockaddr*)&src, &len);
        if(serviceSock < 0)
        {
            logMessage(ERROR, "Accept FAIL, Errno:%d Strerrno:%s", errno, strerror(errno));
            continue;
        }
        // 获取连接成功,开始进行网络通信
        uint16_t clientPort = ntohs(src.sin_port);
        std::string clientIP = inet_ntoa(src.sin_addr);
        logMessage(NORMAL, "Link Success! serviceSock:%d clientIP:%s clientPort:%d", serviceSock, clientIP.c_str(), clientPort);

        pid_t id = fork();
        if(id == 0)
        {
            // 子进程
            close(_listenSock);
            if(fork() > 0) exit(0); // 子进程本身立即退出
            // 因为子进程退出了,那么孙子进程就会北城孤儿进程被1号进程
            // 领养,让操作系统自动释放孙子进程的僵尸状态
            Service(serviceSock, clientIP, clientPort);
            exit(0);
        }
        // 父进程
        close(serviceSock);
        waitpid(id, nullptr, 0); // 此时的waitpid不会阻塞太久
    }
}

TcpClient.cc

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


static void Usage(std::string proc)
{
    std::cout << "\nUsage:" << proc << "serverIP serverPort" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        std::cerr << "Create Socket Fail!" << std::endl;
        exit(2);
    }

    std::string serverIP = argv[1];
    uint16_t serverPort = atoi(argv[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(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    {
        std::cerr << "Connet Fail!" << std::endl;
        exit(3);
    }

    std::cout << "Connet Success!" << std::endl;
    while(true)
    {
        std::string message;
        std::cout << "Please Enter Your Message: ";
        std::getline(std::cin, message);
        send(sock, message.c_str(), message.size(), 0);
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server# " << buffer << std::endl;
        }
        else if(s == 0)
            break;
        else
            break;
    }
    close(sock);

    return 0;
}

TCP 客户端端口号的绑定问题

当客户端程序调用 connect 系统调用时,内核会为客户端分配一个临时的、未绑定的端口号,并将其绑定到客户端套接字描述符对应的网络地址上。需要注意的是,如果客户端希望绑定特定的端口号,可以在调用 connect 之前使用 bind 系统调用来指定端口号。但是,这种情况比较少见,通常情况下客户端会使用动态分配的端口号。

在这里插入图片描述

send、recv 和 sendto、recvfrom 的区别

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

多线程版

多线程版需要注意的细节:

  • 创建出来的线程和主线程都不能够关闭自己不需要文件描述符,因为文件描述符是被所有线程共享的。如果关闭了文件描述符,将会影响到其他线程的执行。
  • 多线程应该进行线程分离,这样主线程就不需要关心多线程的退出状态了。
class ThreadData
{
public:
    uint16_t _port;
    std::string _ip;
    int _sock;
};

class TcpServer
{
    static void* threadRoutine(void* args)
    {
        // 线程分离,主线程不行关心其退出状态
        pthread_detach(pthread_self());
        ThreadData* td = (ThreadData*)args;
        Service(td->_sock, td->_ip, td->_port);
        delete td;
        return nullptr;
    }
public:
    void StartServer()
    {
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // accept函数的返回值是文件描述符,它用于后续的网络通信
            // 而_sock只用于获取新连接,并不用于后续的网络通信
            // 注:accept是阻塞等待新连接的到来
            int serviceSock = accept(_listenSock, (struct sockaddr*)&src, &len);
            if(serviceSock < 0)
            {
                logMessage(ERROR, "Accept FAIL, Errno:%d Strerrno:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功,开始进行网络通信
            uint16_t clientPort = ntohs(src.sin_port);
            std::string clientIP = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Link Success! serviceSock:%d clientIP:%s clientPort:%d", serviceSock, clientIP.c_str(), clientPort);
            // 子线程不能关闭文件描述符,因为多线程场景下文件描述符是公用的
            ThreadData* td = new ThreadData();
            td->_sock = serviceSock;
            td->_port = clientPort;
            td->_ip = clientIP;
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, td);
        }
    }
};

在这里插入图片描述

线程池版

本篇博客使用的线程池相较于线程池的实现,有略微的改动。主要改动如下:类型的重命名,将 Thread.hpp 中的typedefvoid*(*func_t)(void*)改成 typedefvoid*(*Func_t)(void*),以避免与 Task.hpp 中的 func_t 产生命名冲突。还有改动就是将任务类的 Excute 函数改成了 operator(),并给任务类多加了一下成员变量。

任务类

#pragma once

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

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

// 任务类
class Task
{
public:
    Task() = default;

    Task(int sock, const std::string& ip, uint16_t port, func_t func)
        : _sock(sock)
        , _ip(ip)
        , _port(port)
        , _func(func)
    {}

    void operator()(std::string& name)
    {
        _func(_sock, _ip, _port, name);
    }

private:
    int _sock;
    std::string _ip;
    uint16_t _port;
    func_t _func;
};
class TcpServer
{
private:
    const static int backlog = 20; // 全队列长度
public:
    TcpServer(uint16_t port, std::string ip = "")
        : _port(port)
        , _ip(ip)
        , _listenSock(-1)
        , _ptr(ThreadPool<Task>::getThreadPool())
    {}

	// ...

    void StartServer()
    {
        _ptr->Run();
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // accept函数的返回值是文件描述符,它用于后续的网络通信
            // 而_sock只用于获取新连接,并不用于后续的网络通信
            // 注:accept是阻塞等待新连接的到来
            int serviceSock = accept(_listenSock, (struct sockaddr*)&src, &len);
            if(serviceSock < 0)
            {
                logMessage(ERROR, "Accept FAIL, Errno:%d Strerrno:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功,开始进行网络通信
            uint16_t clientPort = ntohs(src.sin_port);
            std::string clientIP = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Link Success! serviceSock:%d clientIP:%s clientPort:%d", serviceSock, clientIP.c_str(), clientPort);
            Task t(serviceSock, clientIP, clientPort, Service);
            _ptr->Push(t);
        }
    }
	// ...
private:
	// ...
    std::unique_ptr<ThreadPool<Task>> _ptr;
};

在这里插入图片描述

关于线程池版的 echo 服务器,需要注意一下几点:

  • 服务器最多同时在线 g_thread_num 人(注:g_thread_num在 threadPool.hpp)中定义,因为服务器和每个客户端建立的都是长连接,而不是短连接。
  • 如果想将线程池版的 echo 服务器改成其他服务,如:在线字典、大小写转换等,只需要修改构建任务时所传的回调函数即可。

👉深入剖析地址转换函数👈

在 Linux 操作系统中,有一些用于进行地址转换的函数,主要用于处理网络通信中的地址格式转换。以下是一些常用的 Linux 网络通信中的地址转换函数:

  • inet_aton 和 inet_addr: 这两个函数用于将点分十进制表示的 IPv4 地址转换为网络字节序的二进制表示。inet_aton 将 IPv4 地址转换为 struct in_addr 类型的结构体,而 inet_addr 则将 IPv4 地址转换为 32 位无符号整数。
    在这里插入图片描述

  • inet_ntoa:这个函数用于将网络字节序的二进制表示的 IPv4 地址转换为点分十进制表示的字符串形式。

  • inet_pton 和 inet_ntop: 这两个函数用于进行 IPv4 和 IPv6 地址之间的二进制表示和文本表示之间的转换。inet_pton 将 IPv4 或 IPv6 地址的字符串表示转换为对应的二进制表示,存储在指定的结构体中。inet_ntop 则将二进制表示的 IPv4 或 IPv6 地址转换为对应的文本表示。

inet_aton 和 inet_ntoa 函数的使用

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

int main()
{
    struct sockaddr_in addr;
    inet_aton("127.0.0.1", &addr.sin_addr);
    uint32_t* ptr = (uint32_t*)&addr.sin_addr;
    printf("addr: %x\n", *ptr);
    printf("addr_str: %s\n", inet_ntoa(addr.sin_addr));

    return 0;
}

inet_pton 和 inet_ntop 函数的使用

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

int main() 
{
    char ip_addr[] = "127.0.0.1";
    struct in_addr addr;

    // 将字符串形式的IPv4地址转换为二进制形式,并存储到addr中
    if (inet_pton(AF_INET, ip_addr, &addr) <= 0) 
    {
        printf("Invalid IP address\n");
        return -1;
    }

    // 输出二进制形式的IP地址
    printf("Binary IP address: 0x%x\n", addr.s_addr);

    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>

int main() 
{
    struct sockaddr_in sa;
    char buffer[INET_ADDRSTRLEN];

    // 设置IPv4地址
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    // 将二进制格式IP地址转换为字符串格式
    const char *ip = inet_ntop(AF_INET, &(sa.sin_addr), buffer, INET_ADDRSTRLEN);

    printf("IP地址:%s\n", buffer);

    return 0;
}

关于 inet_ntoa 函数

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

在这里插入图片描述

man 手册上说,inet_ntoa 函数是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?参见如下代码:

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

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;
}

在这里插入图片描述
因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。

  • 很明显,inet_ntoa 不是一个线程安全的函数,如果有多个线程调用 inet_ntoa 函数,可能会出现异常情况,但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁。
  • 在多线程环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。

测试代码:

#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;
    while (1) 
    {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
        sleep(1);
    }
    return NULL;
}

void* Func2(void* p) 
{
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1) 
    {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
        sleep(1);
    }
    return NULL;
}

int main() 
{
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    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;
}

👉TCP协议通讯流程👈

在这里插入图片描述

TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输协议,其通讯流程如下:

服务器初始化:

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

建立连接

  • 客户端调用 socket 函数,创建文件描述符
  • 客户端调用 connect 函数,向服务端发起连接请求
  • connect 会向服务端发送 SYN 包,并阻塞等待服务器应答(第一次握手)
  • 服务端收到 SYN 包后,回复 ACK+SYN 包,表示已经接收到客户端的请求,并且同意建立连接(第二次握手)
  • 客户端收到 SYN-ACK 包后会从 connect 函数返回,同时应答一个ACK包,表示连接已经建立成功(第三次握手)

数据传输:

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

断开连接:

  • 如果客户端没有更多的请求了,就调用 close 函数关闭连接,客户端会向服务器发送 FIN 包,请求释放连接(第一次挥手)
  • 此时服务器收到 FIN 包后,会回应一个 ACK 包,表示已经接收到客户端的释放请求;同时 read 函数会返回 0,表示客户端关闭了连接(第二次挥手)
  • read 返回之后,服务器就知道客户端关闭了连接,也调用 close 函数关闭连接,这个时候服务器会向客户端发送
    一个 FIN 包,请求释放连接(第三次挥手)
  • 客户端收到 FIN 包,再返回一个 ACK 包给服务器,表示已经接收到服务端的释放请求,连接已经成功关闭(第四次挥手)

建立连接的过程通常称为三次握手,断开连接的过程通常称为四次挥手。

在学习 socket API 时,需要注意应用程序和TCP协议层是如何交互的:

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

TCP和UDP的对比

TCP 和 UDP 都是在网络通信中常用的传输协议,它们之间的主要区别如下:

  • 连接性:TCP 是面向连接的协议,UDP 是无连接的协议。TCP 在通信之前需要先建立连接,而 UDP 则直接发送数据,不需要先建立连接。

  • 可靠性:TCP 是可靠的协议,UDP 是不可靠的协议。TCP 通过三次握手、四次挥手等机制,保证数据的可靠性,数据传输过程中可以进行校验、重传等操作,可以保证数据的完整性。而 UDP 没有这些机制,如果发送的数据丢失或者损坏,就会导致数据的丢失或损坏。

  • 速度:UDP 比 TCP 更快。由于 TCP 需要建立连接和保证可靠性,因此在数据传输过程中需要进行许多额外的操作,导致速度较慢。而 UDP 直接发送数据,没有这些额外的操作,因此速度更快。

  • TCP 是面向字节流的,UDP 是面向数据报的。面向数据包就是对方发一次,我就接收一次;而面向字节流是对方发多次,我一次就全部接收。

👉总结👈

本篇博客基于 TCP 协议编写了单进程版、多进程版、多线程版、线程池版的 echo 服务器、深入剖析地址转换函数以及 TCP 协议的通讯流程等。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️

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

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

相关文章

(二十三)槽函数的书写规则导致槽函数触发2次的问题

在创建QT的信号和槽时&#xff0c;经常无意间保留着QT书写槽函数的习惯&#xff0c;或者在QT设计界面直接右键【转到槽】去创建槽函数&#xff0c;但是后期需要用到disconnect时&#xff0c;又重新写了一遍connect函数&#xff0c;那么你会发现实际槽函数执行了2遍。 首先来看…

要在Ubuntu中查找进程的PID,可以使用pgrep或pidof命令。

一 查找进程 1.pgrep命令 pgrep命令可以根据进程名或其他属性查找进程的PID。例如&#xff0c;要查找名为"firefox"的进程的PID&#xff0c;可以在终端中输入以下命令&#xff1a; pgrep firefox如果有多个名为"firefox"的进程&#xff0c;pgrep命令将返…

互联网一个赛道只剩下几家,真要爆品

互联网一个赛道剩下几家&#xff0c;真要爆品 2017年的书&#xff0c;案例基本上是马后炮总结 趣讲大白话&#xff1a;说起来容易&#xff0c;做起来难 【趣讲信息科技136期】 **************************** 书中讲的范冰冰翻车了 书中不看好的线下渠道&#xff0c;现在成香饽饽…

面试篇-Java并发之CAS:掌握原理、优缺点和应用场景分析,避免竞态问题

1、CAS介绍及原理 多线程中的CAS&#xff08;Compare-and-Swap&#xff09;操作是一种常见的并发控制方法&#xff0c;用于实现原子性更新共享变量的值。其核心思想是通过比较内存地址上的值和期望值是否相等来确定是否可以进行更新操作&#xff0c;从而避免多线程条件下的竞态…

HMI实时显示网络摄像机监控画面——以海康威视网络摄像机为例

随着IOT技术的快速发展&#xff0c;网络摄像机快速应用于工业领域&#xff0c;结合其他智能设备建立一个智能系统&#xff0c;提高用户与机器设备之间的交互体验&#xff0c;帮助企业优化人员配置。 作为重要的可视化设备&#xff0c;HMI不仅可以采集现场设备数据&#xff0c;…

uniapp系列-使用uniapp携带收件人信息调用手机邮件应用发邮件的2种方案

背景描述 我们使用uniapp打包之后&#xff0c;某些情况下&#xff0c;需要使用uniapp打开手机其他应用去发邮件&#xff0c;携带对方email 信息以及主题信息等&#xff0c;那我们应该怎么处理呢&#xff1f; 方案一&#xff1a;使用uniapp标签-uni-link&#xff0c;注意这种方…

BGP实验(一)

实验要求&#xff1a; 1、As1存在两个环回&#xff0c;一个地址为192.168.1.0/24&#xff0c;该地址不能在任何协议中宣告&#xff0c; As3存在两个环回,.一个地址为192.168.2.0/24&#xff0c;该地址不能在任何协议中宣告&#xff0c; As1还有一个环回地址为10.1.1.0/24&…

研读Rust圣经解析——Rust learn-8(match,if-let简洁控制流,包管理)

研读Rust圣经解析——Rust learn-8&#xff08;match,if-let简洁控制流&#xff0c;包管理&#xff09;matchother和占位符_区别easy matchenum matchno valuematch innerOption matchmore better wayif-let整洁控制包管理模块(mod)拆分声明modpub公开use展开引用拆解模块结构m…

docker cmd

sudo docker run --gpus all --name uavrl1 themvs/uav_swarm_reinforcement_learning sudo docker p s-a 86850d5a9dc3 sudo docker run --gpus all --name uavrl12 uavrl:v1.2 ---------- 共享屏幕输入类似指令&#xff0c;实测可行 sudo docker run -it --nethost --ipc…

Leetcode每日一题——“轮转数组”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容是轮转数组&#xff0c;下面&#xff0c;让我们进入轮转数组的世界吧 小雅兰之前其实就已经写过了字符串旋转的问题了&#xff1a; C语言刷题&#xff08;7&#xff09;&#xff08;字符串旋转问题&#xff09…

优化 Kafka 的生产者和消费者

背景 如今&#xff0c;分布式架构已经成为事实上的架构模范&#xff0c;这使得通过 REST API 和 消息中间件来降低微服务之间的耦合变得必然。就消息中间件而言&#xff0c;Apache Kafka 已经普遍存在于如今的分布式系统中。Apache Kafka 是一个强大的、分布式的、备份的消息服…

HBase高手之路5—HBase的JavaAPI编程

文章目录Hbase高手之路5—Hbase的JavaAPI编程一、需求与数据集二、准备工作1.下载安装Java2.下载安装Idea3.下载安装maven4.Maven配置国内的镜像库5.Idea使用自定义的maven配置6.创建一个maven测试项目7.创建所需要的包8.创建类文件&#xff0c;输入代码9.运行项目三、创建HBas…

【2023 年第十三届 MathorCup 高校数学建模挑战赛】A 题 量子计算机在信用评分卡组合优化中的应用 详细建模过程解析及代码实现

更新信息&#xff1a;2023-4-15 更新了代码 【2023 年第十三届 MathorCup 高校数学建模挑战赛】A 题 量子计算机在信用评分卡组合优化中的应用 更新信息&#xff1a;2023-4-15 更新了代码 1 题目 在银行信用卡或相关的贷款等业务中&#xff0c;对客户授信之前&#xff0c;需…

Linux程序的内存

要研究程序的运行环境&#xff0c;首先要弄明白程序与内存的关系。程序与内存的关系&#xff0c;好比鱼和水一般密不可分。内存是承载程序运行的介质&#xff0c;也是程序进行各种运算和表达的场所。了解程序如何使用内存&#xff0c;对程序本身的理解&#xff0c;以及后续章节…

【CSS-Part3 样式显示模式、背景设置、三大特性 】

CSS-Part3 样式显示模式、背景设置、三大特性一 CSS元素显示模式&#xff1a;1.1块元素&#xff1a;1.2行内元素&#xff1a;1.3行内块元素&#xff1a;(同时具有行内元素和块元素的特点)元素显示模式总结&#xff1a;1.4元素显示模式转换&#xff1a;一种模式的元素需要另一模…

从Navicat 和 DBeaver中导出数据不要文本识别符号 “”

今天需要从MySQL和ClickHouse数据库中导出CSV数据文件&#xff0c;打开CSV数据文件后发现字段的数据带着""这种不需要的符号&#xff0c;研究了一下终于成功导出了不要文本识别符号“”的CSV文件 一、演示从DBeaver导出ClickHouse数据库的表文件 第一步&#xff0c…

SSH、OpenSSH、SSL、OpenSSL及CA

OpenSSL1. SSH、OpenSSH、SSL、OpenSSL关系及区别2. SSH介绍2.1 概念2.2 SSH的主要功能2.3 示例讲解2.4 ssh和sshd的区别3. OpenSSH介绍3.1 概念3.2 OpenSSH程序简介3.3 OpenSSH 包含的组件1. ssh2. scp3. sftp4. sshd5. ssh-keygen6. ssh-copy-id7. ssh-agent8. ssh-add9. ssh…

刘二大人《Pytorch深度学习实践》第九讲多分类问题

文章目录多分类问题损失函数课上代码transforms的使用方法view&#xff08;&#xff09;函数dim维度的理解为什么要使用item()多分类问题 把原来只有一个输出&#xff0c;加到10个 每个输出对应一个数字&#xff0c;这样可以得到每个数字对应的概率值&#xff0c;这里每个输出做…

Netty实战与调优

Netty实战与调优 聊天室业务介绍 代码参考 /*** 用户管理接口*/ public interface UserService {/*** 登录* param username 用户名* param password 密码* return 登录成功返回 true, 否则返回 false*/boolean login(String username, String password); }/*** 会话管理接口…

如何快速上手Vue框架?

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 运行环境&#xff1a;Google浏览器 Vue框架版本&#xff1a;Vue.js v2.7.14 目录一. 框架是什么&#xff1f;二. 怎么写一个Vue程序&#xff08;以IDEA举例&#xff09;&#xff1f;三. 什么是声明式渲染?3.1 声明式3.2 渲染四…