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

这篇具有很好参考价值的文章主要介绍了【Linux】揭开套接字编程的神秘面纱(下)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
【Linux】揭开套接字编程的神秘面纱(下)

👉前言👈

在揭开套接字编程神秘面纱(上)中,我们已经学习到了套接字编程的相关基础知识以及编写了基于 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 参数的取值应该根据服务器的处理能力和网络环境等因素进行合理的设置,以确保服务器可以及时处理连接请求,同时又不会占用过多的系统资源。

查看网络状态

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

Telnet 协议

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

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

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

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

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

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

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

【Linux】揭开套接字编程的神秘面纱(下)
【Linux】揭开套接字编程的神秘面纱(下)
单进程版的 echo 服务器的细节

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

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

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

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

为什么多个子进程所用于通信的套接字(文件描述符)都是相等的呢?因为父进程会关闭自己所不需要的文件描述符,这个不需要的文件描述符就是 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 系统调用来指定端口号。但是,这种情况比较少见,通常情况下客户端会使用动态分配的端口号。

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

send、recv 和 sendto、recvfrom 的区别

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

多线程版

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

  • 创建出来的线程和主线程都不能够关闭自己不需要文件描述符,因为文件描述符是被所有线程共享的。如果关闭了文件描述符,将会影响到其他线程的执行。
  • 多线程应该进行线程分离,这样主线程就不需要关心多线程的退出状态了。
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);
        }
    }
};

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

线程池版

本篇博客使用的线程池相较于线程池的实现,有略微的改动。主要改动如下:类型的重命名,将 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;
};

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

关于线程池版的 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 位无符号整数。
    【Linux】揭开套接字编程的神秘面纱(下)

  • 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 的结果. 那么是否需要调用者手动释放呢?

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

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

【Linux】揭开套接字编程的神秘面纱(下)
因为 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协议通讯流程👈

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

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 协议的通讯流程等。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️文章来源地址https://www.toymoban.com/news/detail-410946.html

到了这里,关于【Linux】揭开套接字编程的神秘面纱(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【Linux】TCP套接字编程

    【Linux】TCP套接字编程

    目录 前言 UDP服务器的完善 线程的封装 结构定义 接口实现 环形队列 结构定义 接口实现 加锁 信号量的申请与释放 入队与出队 整体组装  初始化与析构 信息接收线程 消息发送线程 TCP套接字 创建套接字 listen accept 收发操作 客户端的编写 进一步完善 多进程 多线程 总结 上篇

    2024年02月05日
    浏览(8)
  • 【Linux】网络---->套接字编程(TCP)

    【Linux】网络---->套接字编程(TCP)

    TCP的编程流程:大致可以分为五个过程,分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。 1.准备过程:服务端和客户端需要创建各自的套接字,除此之外服务端还需要绑定自己的地址信息和进行监听。注意:服务端调用listen函数后,处理监听状

    2024年02月04日
    浏览(39)
  • 【Linux】网络编程套接字一

    【Linux】网络编程套接字一

    上篇博客由唐僧的例子我们知道: 在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。 思考一下: 不考虑中间的一系列步骤,两台主机我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上。 但是我们把

    2024年03月26日
    浏览(449)
  • Linux网络编程(二-套接字)

    Linux网络编程(二-套接字)

    目录 一、背景知识 1.1 端口号 1.2 网络字节序 1.3 地址转换函数  二、Socket简介 三、套接字相关的函数  3.1 socket() 3.2 bind() 3.3 connect() 3.4 listen() 3.5 accept()  3.6 read()/recv()/recvfrom() 3.7 send()/sendto()  3.8 close()  四、UPD客服/服务端实验  1.1 端口号 端口号是访问服务器的标识 ,就好像

    2024年01月22日
    浏览(72)
  • 【Linux网络编程】网络编程套接字二

    【Linux网络编程】网络编程套接字二

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

    2024年04月15日
    浏览(45)
  • Linux网络编程——tcp套接字

    Linux网络编程——tcp套接字

    本章Gitee仓库:tcp套接字 客户端: 客户端: 关于构造和初始化,可以直接在构造的时候,将服务器初始化,那为什么还要写到 init 初始化函数里面呢? 构造尽量简单一点,不要做一些“有风险”的操作。 tcp 是面向连接的,通信之前要建立连接,服务器处于等待连接到来的

    2024年02月20日
    浏览(41)
  • 【Linux网络】网络编程套接字(TCP)

    【Linux网络】网络编程套接字(TCP)

    目录 地址转换函数 字符串IP转整数IP 整数IP转字符串IP 关于inet_ntoa 简单的单执行流TCP网络程序 TCP socket API 详解及封装TCP socket  服务端创建套接字  服务端绑定  服务端监听  服务端获取连接  服务端处理请求 客户端创建套接字 客户端连接服务器 客户端发起请求 服务器测试

    2024年03月21日
    浏览(38)
  • linux【网络编程】之网络套接字预备

    linux【网络编程】之网络套接字预备

    在【网络基础】中我们提到了IP地址,接下来了解一下网络通信中其他方面的知识 端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; 一个端口号只能被一个进程占用 通信原理 (公网)IP唯一标识一台主机,这样两台

    2024年02月05日
    浏览(121)
  • 【Linux】网络基础+UDP网络套接字编程

    【Linux】网络基础+UDP网络套接字编程

    只做自己喜欢做的事情,不被社会和时代裹挟着前进,是一件很奢侈的事。 1. 首先计算机是人类设计出来提高生产力的工具,而人类的文明绵延至今一定离不开人类之间互相的协作,既然人类需要协作以完成更为复杂的工作和难题,所以计算机作为人类的工具自然也一定需要

    2024年02月08日
    浏览(39)
  • 【Linux网络】网络编程套接字(预备知识+UDP)

    【Linux网络】网络编程套接字(预备知识+UDP)

    目录 预备知识 1. 理解源IP地址和目的IP地址 2. 理解源MAC地址和目的MAC地址 3. 认识端口号  4. 理解源端口号和目的端口号 5. 端口号(port) vs 进程pid 6. 认识TCP协议和认识UDP协议 7. 网络字节序 socket编程接口  1. socket 常见API 2. sockaddr结构  简单的UDP网络程序  1. 服务端创建udp

    2024年02月19日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包