草庐IT

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

阿亮joy. 2023-04-25 原文

​🌠 作者:@阿亮joy.
🎆专栏:《学会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 参数的取值应该根据服务器的处理能力和网络环境等因素进行合理的设置,以确保服务器可以及时处理连接请求,同时又不会占用过多的系统资源。

查看网络状态

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 协议的通讯流程等。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️

有关【Linux】揭开套接字编程的神秘面纱(下)的更多相关文章

  1. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  2. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  3. ruby - 是否可以在不实际发送或读取数据的情况下查明 ruby​​ 套接字是否处于 ESTABLISHED 或 CLOSE_WAIT 状态? - 2

    s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成

  4. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  5. ruby-on-rails - Ruby 的 'open_uri' 是否在读取或失败后可靠地关闭套接字? - 2

    一段时间以来,我一直在使用open_uri下拉ftp路径作为数据源,但突然发现我几乎连续不断地收到“530抱歉,允许的最大客户端数(95)已经连接。”我不确定我的代码是否有问题,或者是否是其他人在访问服务器,不幸的是,我无法真正确定谁有问题。本质上,我正在读取FTPURI:defself.read_uri(uri)beginuri=open(uri).readuri=="Error"?nil:urirescueOpenURI::HTTPErrornilendend我猜我需要在这里添加一些额外的错误处理代码...我想确保我采取一切预防措施来关闭所有连接,这样我的连接就不是问题所在,但是我

  6. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  7. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  8. Ruby 元编程问题 - 2

    我正在查看Ruby日志记录库Logging.logger方法并从sourceatgithub提出问题与这段代码有关:logger=::Logging::Logger.new(name)logger.add_appendersappenderlogger.additive=falseclass我知道类 最佳答案 这实际上删除了方法(当它实际被执行时)。这是确保close不会被调用两次的保障措施。看起来好像有嵌套的“class 关于Ruby元编程问题,我们在StackOverflow上找到一

  9. ruby - Paperclip:以编程方式分配图像并设置其名称 - 2

    使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案

  10. ruby - 如何以编程方式检查证书是否已被吊销? - 2

    我正在开发一个xcode自动构建系统。在执行一些预构建验证时,我想检查指定的证书文件是否已被撤销。我了解securityverify-cert验证其他证书属性但不验证吊销。我如何检查撤销?我正在用Ruby编写构建系统,但我对任何语言的想法都持开放态度。我阅读了这个答案(Openssl-Howtocheckifacertificateisrevokedornot),但指向底部的链接(DoesOpenSSLautomaticallyhandleCRLs(CertificateRevocationLists)now?)进入的Material对我的目的来说有点过于复杂(用户上传已撤销的证书是一

随机推荐