Linux网络编程:TCP与UDP详解


今天是一篇关于socket网络编程的原创文章。虽然Linux上的socket网络编程技术多用于服务器编程,但其实客户端中也有使用这个技术的关键场景:长连接。比如笔者所在项目的客户端,其长连接也是使用socket的c++编程实现的。基于TCP协议的socket编程实现非常适合需要轻量稳定的客户端长连接。

Linux网络编程中,TCP和UDP是两种主要的传输层协议。本文将详细分析TCP和UDP在网络编程中的使用、原理、代码示例、数据流动,一些异常情况的处理方式,以及如何使用socket编程实现客户端长连接。

一、TCP与UDP概述

1.1 TCP的原理

TCP是一种面向连接的协议,它通过三次握手建立连接,然后在连接上进行可靠的数据传输。TCP使用序列号和确认应答(ACK)来保证数据的可靠传输,通过滑动窗口和拥塞控制算法进行流量控制和拥塞控制。

1.2 UDP的原理

相比于TCP,UDP是一种更简单的协议。UDP是无连接的,它直接在IP协议之上发送数据报,不提供数据的可靠传输、流量控制或拥塞控制。因此,UDP的延迟和开销较小,适用于对实时性要求高的应用,如语音和视频通信。

1.3 数据流动

在TCP和UDP通信中,数据是从客户端流向服务器的。客户端首先建立连接(TCP)或直接发送数据报(UDP),然后服务器接收并处理这些数据,可能会返回响应给客户端。在TCP通信中,数据的流动是双向的,客户端和服务器都可以发送数据和接收数据。在UDP通信中,数据的流动也是双向的,但是由于UDP是无连接的,客户端和服务器可以独立地发送和接收数据。

二、Socket的使用

在Linux网络编程中,我们使用socket来实现TCP和UDP通信。以下是TCP和UDP的socket使用示例:

2.1 TCP Socket示例

服务器端:

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    listen(server_fd, 5);

    while (true) {
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

        char buffer[1024];
        ssize_t read_len = read(client_fd, buffer, sizeof(buffer) - 1);
        buffer[read_len] = '\0';
        std::cout << "Received: " << buffer << std::endl;

        write(client_fd, buffer, strlen(buffer));

        close(client_fd);
    }

    close(server_fd);
    return 0;
}

客户端:

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

int main() {
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    const char *message = "Hello, Server!";
    write(client_fd, message, strlen(message));

    char buffer[1024];
    ssize_t read_len = read(client_fd, buffer, sizeof(buffer) - 1);
    buffer[read_len] = '\0';
    std::cout << "Received: " << buffer << std::endl;

    close(client_fd);
    return 0;
}

在Linux网络编程中,socket(), sockaddr_in 结构体和相关常量都是用于创建和配置套接字的关键组件。以下是上面代码的含义和用法:

  • AF_INET:这是一个地址族(Address Family)常量,表示我们使用的是IPv4协议。在创建套接字时,需要指定地址族以确定使用哪种协议。另一个常见的地址族是AF_INET6,表示使用IPv6协议。

  • SOCK_STREAM:这是一个套接字类型(Socket Type)常量,表示我们使用的是面向连接的、可靠的字节流。在TCP协议中,我们使用SOCK_STREAM类型的套接字。另一个常见的套接字类型是SOCK_DGRAM,表示无连接的、不可靠的数据报文,通常用于UDP协议。

  • socket(AF_INET, SOCK_STREAM, 0):这是一个系统调用,用于创建一个新的套接字。它接受三个参数:地址族(如AF_INET)、套接字类型(如SOCK_STREAM)和协议(通常设置为0,让系统自动选择协议,如TCP或UDP)。此函数返回一个套接字文件描述符,用于后续的网络操作。

  • struct sockaddr_in:这是一个用于表示IPv4套接字地址的结构体。它包含了地址族、端口号和IPv4地址。在网络编程中,我们需要使用此结构体来设置服务器和客户端的地址信息。

  • server_addr.sin_family = AF_INET:设置sockaddr_in结构体中的地址族字段为AF_INET,表示使用IPv4协议。

  • server_addr.sin_port = htons(8080):设置sockaddr_in结构体中的端口号字段。htons()函数将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order)。这里我们设置端口号为8080。

  • INADDR_ANY:这是一个特殊的IPv4地址(0.0.0.0),表示服务器将监听所有可用的网络接口。当服务器有多个网络接口时,使用INADDR_ANY可以让服务器接受来自任何接口的连接请求。

  • server_addr.sin_addr.s_addr = INADDR_ANY:设置sockaddr_in结构体中的IPv4地址字段为INADDR_ANY,表示服务器将监听所有可用的网络接口。

2.2 UDP Socket示例

服务器端:

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

int main() {
    int server_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    while (true) {
        char buffer[1024];
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        ssize_t read_len = recvfrom(server_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);
        buffer[read_len] = '\0';
        std::cout << "Received: " << buffer << std::endl;

        sendto(server_fd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_addr_len);
    }

    close(server_fd);
    return 0;
}

客户端:

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

int main() {
    int client_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    const char *message = "Hello, Server!";
    sendto(client_fd, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));

    char buffer[1024];
    struct sockaddr_in recv_addr;
    socklen_t recv_addr_len = sizeof(recv_addr);
    ssize_t read_len = recvfrom(client_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&recv_addr, &recv_addr_len);
    buffer[read_len] = '\0';
    std::cout << "Received: " << buffer << std::endl;

    close(client_fd);
    return 0;
}

三、数据流动时序图

以下是TCP和UDP通信的时序图,展示了客户端与服务器之间的数据流动。

3.1 TCP通信详解

在TCP通信中,我们首先需要建立一个TCP连接,然后才能在这个连接上进行数据传输。以下是TCP通信的详细步骤和时序图:

  1. 服务器执行socket()函数,创建一个新的套接字。
  2. 服务器执行bind()函数,将套接字绑定到一个指定的地址(包括IP地址和端口号)。
  3. 服务器执行listen()函数,使套接字进入监听模式,等待客户端的连接请求。
  4. 服务器执行accept()函数,阻塞并等待客户端的连接请求。当一个客户端连接请求到来时,accept()函数返回,并创建一个新的套接字与客户端进行通信。
  5. 客户端执行socket()connect()函数,向服务器发起连接请求。connect()函数会发送一个SYN(同步)数据包到服务器。
  6. 服务器收到SYN数据包,在accept()函数返回后,回复一个SYN+ACK(确认应答)数据包给客户端。
  7. 客户端收到SYN+ACK数据包,回复一个ACK数据包给服务器,完成TCP连接的建立。
  8. TCP连接建立后,客户端和服务器可以通过read()write()函数进行数据传输。

以下是TCP通信的时序图:

Server                Client
  |                     |
  | socket()            |
  |                     |
  | bind()              |
  |                     |
  | listen()            |
  |                     |
  | accept()            |
  |                     |
  |--等待客户端连接请求--->|
  |                     |
  |                     |
  | socket(), connect() | 
  |<--- SYN ------------|
  |                     |
  |-- SYN + ACK ------->|
  |                     |
  |<--- ACK ------------|
  |                     |
  |<-- Data ------------|
  | read(), write()     |
  |                     |
  |-- Data -----------> |
  | read(), write()     |
  |                     |

3.2 UDP通信详解

与TCP不同,UDP是一种无连接的协议,客户端和服务器不需要建立连接就可以直接发送数据。以下是UDP通信的详细步骤:

  1. 服务器执行socket()函数,创建一个新的套接字。
  2. 服务器执行bind()函数,将套接字绑定到一个指定的地址(包括IP地址和端口号)。
  3. 客户端执行socket()函数,创建一个新的套接字。
  4. 客户端可以直接通过sendto()函数发送数据到服务器。
  5. 服务器通过recvfrom()函数接收客户端发送的数据。

以下是UDP通信的时序图:

Server                Client
  |                     |
  | socket()            |
  |                     |
  | bind()              |
  |                     |
  |----等待客户端数据---->|
  |                     |
  |                     |
  |   	     socket()|
  | 	     sendto()| 
  |<--- Data -----------|
  | recvfrom()          |
  |                     |

在这种情况下,服务器已经准备好接受客户端的数据。当客户端执行socket()sendto()函数发送数据时,服务器会通过recvfrom()函数接收这些数据。

四、客户端长连接

笔者所在客户端项目的网络层是用C++实现,其中的长连接部分就是用socket接口编程实现的。本节就来看看如何在客户端使用socket和服务器建立连接、读取和发送数据。

4.1 建立长连接

下面是使用socket建立连接的实现,AsyncSocket::connect函数用于异步连接目标服务器:

  • 检查是否在正确的线程上调用并设置连接状态和回调函数。
  • 解析主机名(如果需要)并遍历地址列表,尝试创建套接字。
  • 设置套接字选项并调用连接前的回调函数。
  • 尝试连接目标地址并处理错误码。
  • 监听套接字的可写事件,以便在连接成功时通知。
int AsyncSocket::connect(const CompletionCallback& callback, const BeforeConnectCallback& beforeConnectCallback) {
    // 检查是否在正确的线程上调用
    DCHECK(thread_checker_.CalledOnValidThread());
    // 检查连接状态是否为 CONNECT_STATE_NONE
    DCHECK(connect_state_ == CONNECT_STATE_NONE);

    // 设置连接状态为 CONNECT_STATE_CONNECT
    connect_state_ = CONNECT_STATE_CONNECT;
    // 设置回调函数
    write_callback_ = callback;
    before_connect_callback_ = beforeConnectCallback;

    // 如果主机名不为空且地址列表为空,则解析主机名
    if (hostname_.length() && sockaddrs_.empty()) {
        int ret = resolveHostBeforeConnect();
        if (ret != 0) return ERR_IO_PENDING;
    }

    // 遍历地址列表,尝试创建套接字
    while (!sockaddrs_.empty()) {
        hostaddr_ = sockaddrs_.front();
        sockaddrs_.erase(sockaddrs_.begin());

        struct sockaddr_storage *addr = (struct sockaddr_storage *)hostaddr_.data();
        LOG(WARNING) << "AsyncSocket connect IP: " << Sockaddr2IP((const sockaddr*)addr);
        socket_fd_ = socket(addr->ss_family, SOCK_STREAM, IPPROTO_TCP);
        if (socket_fd_ < 0) {
            // 如果创建套接字失败,根据错误码输出错误日志
            switch errno {
            case EAFNOSUPPORT:
                LOG(ERROR) << "AsyncSocket create socket encounter EAFNOSUPPORT";
                break;
            case EPROTONOSUPPORT:
                LOG(ERROR) << "AsyncSocket create socket encounter EPROTONOSUPPORT";
                break;
            case ENFILE:
                LOG(ERROR) << "AsyncSocket create socket encounter ENFILE";
                break;
            case EMFILE:
                LOG(ERROR) << "AsyncSocket create socket encounter EMFILE";
                break;
            default:
                LOG(ERROR) << "AsyncSocket create socket returned an error, errno=" << errno;
                break;
            }
        } else {
            break;
        }
    }

    // 如果套接字创建失败,调用回调函数并返回错误码
    if (socket_fd_ < 0) {
        if (!beforeConnectCallback.is_null()) beforeConnectCallback.Run(false);
        if (!callback.is_null()) callback.Run(ERR_ADDRESS_INVALID);
        return socket_fd_;
    }

    // 设置套接字选项
    SetNonBlocking(socket_fd_);
    SetTCPNoDelay(socket_fd_, true);
    SetTCPKeepAlive(socket_fd_, true, 900);
    SetTCPNoSigPipe(socket_fd_);
    SetTCPTimeout(socket_fd_, 15);

    // 调用连接前的回调函数
    if (!beforeConnectCallback.is_null()) beforeConnectCallback.Run(true);
    // 尝试连接目标地址
    int rv = ::connect(socket_fd_, (sockaddr *)hostaddr_.data(), (socklen_t)hostaddr_.length());
    // 将错误码映射为对应的错误类型
    int ret = rv == 0 ? OK : MapConnectError(errno);

    // 如果连接不需要等待,则直接调用回调函数并返回结果
    if (ret != ERR_IO_PENDING) {
        callback.Run(ret);
        return ret;
    }

    // 监听套接字的可写事件,以便在连接成功时通知
    if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
        socket_fd_, true, base::MessageLoopForIO::WATCH_WRITE,
        &write_socket_watcher_, this)) {
        PLOG(ERROR) << "WatchFileDescriptor failed on connect, errno " << errno;
        return errno;
    }
    return ret;
}

通过这个函数,可以实现异步连接目标服务器,并在连接成功或失败时调用相应的回调。

4.2 长连接上的数据读写

建立完长连接后,基于socket实现异步读写如下,AsyncSocket::readAsyncSocket::write分别用于异步读取和写入数据:

  • 检查是否在正确的线程上调用、回调函数是否为空以及缓冲区长度是否大于 0。
  • 检查连接状态并调用系统函数读取或写入数据。
  • 处理返回值或错误码,并在结果不为 0 且不为ERR_IO_PENDING时,调用回调函数并返回结果。
  • 监听套接字的可读(对于AsyncSocket::read)或可写(对于AsyncSocket::write)事件,以便在数据到来时通知。
  • 保存缓冲区、长度和回调函数,并返回ERR_IO_PENDING表示操作正在进行。
// AsyncSocket::read函数,用于从socket读取数据
int AsyncSocket::read(IOBuffer* buf, size_t buf_len, const CompletionCallback& callback) {
    // 检查是否在正确的线程上调用
    DCHECK(thread_checker_.CalledOnValidThread());
    // 检查读取回调是否为空
    CHECK(read_callback_.is_null());
    // 不支持同步操作,回调函数不能为空
    DCHECK(!callback.is_null());
    // 检查缓冲区长度是否大于0
    DCHECK_LT(0, buf_len);
    
    // 检查连接状态
    if (connect_state_ != CONNECT_STATE_CONNECT_COMPLETE) {
        // 如果连接未完成,回调并返回错误
        callback.Run(ERR_SOCKET_NOT_CONNECTED);
        return 0;
    }
    
    // 调用系统read函数读取数据
    ssize_t rv = ::read(socket_fd_, buf->data(), buf_len);
    // 将返回值或错误码映射为结果
    int ret = rv >= 0 ? (int)rv : MapSystemError(errno);
    // 如果结果不为0且不为ERR_IO_PENDING,回调并返回结果
    if (ret != 0 && ret != ERR_IO_PENDING) {
        callback.Run(ret);
        return ret;
    }
    
    // 监听套接字的可读事件,以便在数据到来时通知
    if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
        socket_fd_, true, base::MessageLoopForIO::WATCH_READ,
        &read_socket_watcher_, this)) {
        PLOG(ERROR) << "WatchFileDescriptor failed on read, errno " << errno;
        return errno;
    }
    
    // 保存缓冲区、长度和回调函数
    read_buf_ = buf;
    read_buf_len_ = buf_len;
    read_callback_ = callback;
    // 返回ERR_IO_PENDING表示操作正在进行
    return ERR_IO_PENDING;
}

// AsyncSocket::write函数,用于向socket写入数据
int AsyncSocket::write(IOBuffer* buf, size_t buf_len, const CompletionCallback& callback) {
    // 检查是否在正确的线程上调用
    DCHECK(thread_checker_.CalledOnValidThread());
    // 检查连接状态是否为CONNECT_STATE_CONNECT_COMPLETE
    DCHECK(connect_state_ == CONNECT_STATE_CONNECT_COMPLETE);
    // 检查写入回调是否为空
    CHECK(write_callback_.is_null());
    // 不支持同步操作,回调函数不能为空
    DCHECK(!callback.is_null());
    // 检查缓冲区长度是否大于0
    DCHECK_LT(0, buf_len);
    
    // 调用WriteWrapper函数写入数据
    ssize_t rv = WriteWrapper(socket_fd_, buf->data(), buf_len);
    // 将返回值或错误码映射为结果
    int ret = rv >= 0 ? (int)rv : MapSystemError(errno);
    // 如果结果不为0且不为ERR_IO_PENDING,回调并返回结果
    if (ret != 0 && ret != ERR_IO_PENDING) {
        callback.Run(ret);
        return ret;
    }
    
    // 监听套接字的可写事件,以便在可以写入数据时通知
    if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
        socket_fd_, true, base::MessageLoopForIO::WATCH_WRITE,
        &write_socket_watcher_, this)) {
        PLOG(ERROR) << "WatchFileDescriptor failed on write, errno " << errno;
        return errno;
    }
    
    // 保存缓冲区、长度和回调函数
    write_buf_ = buf;
    write_buf_len_ = buf_len;
    write_callback_ = callback;
    // 返回ERR_IO_PENDING表示操作正在进行
    return ERR_IO_PENDING;
}

通过这两个函数,可以实现异步读取和写入数据。当数据准备好读取或可以写入时,将调用相应的回调函数。这样可以避免阻塞操作,提高程序的性能和响应速度。

五、异常情况处理

在网络通信中,可能会遇到一些异常情况,如TCP握手过程中服务器ACK丢失、第三次握手的ACK丢失等。以下是这些异常情况的处理方式:

5.1 第一次握手的服务器ACK丢失

当第一次握手服务器发送的ACK丢失时,客户端将无法收到确认,因此会重新发送SYN。服务器在收到重复的SYN后,会再次发送ACK。这个过程会持续进行,直到客户端收到ACK或达到最大重传次数。

5.2 第三次握手的ACK丢失

当TCP握手过程中第三次握手的ACK丢失时,客户端和服务器分别会发生以下情况:

  1. 服务器端:

服务器端在收到客户端的SYN包后,会回复SYN+ACK包,并进入SYN_RCVD状态。此时,服务器端将该连接放入半连接队列。当第三次握手的ACK丢失时,服务器端没有收到客户端的ACK包,服务器端会认为连接尚未建立。根据TCP的超时重传机制,服务器端会等待一定时间(例如3秒、6秒、12秒等),然后重新发送SYN+ACK包给客户端,期待客户端重新发送ACK包。

在等待过程中,服务器端会继续监听其他新的连接请求。如果重试次数达到上限(例如5次),服务器端可能会放弃等待,将该连接从半连接队列中移除。

  1. 客户端:

客户端在收到服务器端的SYN+ACK包后,会发送ACK包并将连接状态设置为ESTABLISHED。然而,如果第三次握手的ACK丢失,服务器端无法收到该ACK包。在这种情况下,客户端会在超时后尝试重新发送ACK包,以确保服务器端收到。通常,客户端会在等待一段时间后触发重传机制,避免长时间等待。

在重试过程中,如果服务器端已经因为半连接队列超时而将该连接移除,当服务器端收到客户端重新发送的ACK包时,服务器端可能会回复一个RST包(用于强制关闭TCP连接),此时客户端会感知到连接建立失败。

总之,当第三次握手的ACK丢失时,服务器端和客户端都会尝试重新发送SYN+ACK包和ACK包,以确保连接能够成功建立。这种重试机制可以提高TCP连接的可靠性,确保即使在网络不稳定的情况下,连接仍然能够建立。

六、总结

本文详细讨论了Linux网络编程中TCP和UDP两种方式的socket使用、原理分析、代码示例、数据流动时序图,一些异常情况的处理方式,以及如何使用socket编程实现客户端长连接。理解这些概念有助于更高效地进行socket网络编程,应对各种网络通信场景。

推荐阅读

TCP与UDP:网络协议的技术原理与要点
从HTTP到QUIC:网络协议的演进与优化
HTTPS:原理、使用方法及安全威胁

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Linux网络编程中的TCPUDP是两种常见的传输协议。 TCP(Transmission Control Protocol)是一种基于连接的可靠传输协议。它提供了面向连接、可靠的数据传输服务。在TCP通信中,数据被分割成小的数据块,通过TCP连接按序传输,并且保证数据的可靠性,即使在网络拥塞或数据丢失的情况下也能重新传输丢失的数据。TCP适用于对可靠性要求较高的应用程序,如文件传输、电子邮件和网页浏览。 UDP(User Datagram Protocol)是一种无连接的不可靠传输协议。它提供了一种无序、不可靠的数据传输服务。在UDP通信中,数据以数据包(也称为数据报)的形式发送,不进行连接建立和断开,也不保证数据的可靠性和按序传输。UDP适用于对实时性要求较高、对数据可靠性要求较低的应用程序,如音视频流媒体、在线游戏等。 在Linux中进行TCPUDP网络编程可以使用Socket API。该API提供了一组函数和数据结构,用于创建套接字(socket)、绑定(bind)套接字到特定的IP地址和端口、监听(listen)连接请求、接受(accept)连接、建立连接(connect)、发送(send)和接收(receive)数据等操作。 你可以使用C语言或其他支持Socket API的编程语言来进行Linux网络编程,通过调用Socket API提供的函数来实现TCPUDP通信。在编程过程中,你需要了解TCPUDP的特点、使用套接字创建相应的连接类型、发送和接收数据的方式等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陆业聪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值