Linux网络-传输层UDP/TCP详解

目录

计算机网络的层状结构

UDP协议

UDP报文格式

理解UDP/TCP报文的本质

UDP的特点

UDP的缓冲区

sendto/recvfrom/send/recv/write/read IO类接口

UDP是全双工的

UDP注意事项

UDP协议,实现简单聊天室(服务端+客户端)

TCP协议

TCP协议段格式

确认应答(ACK)机制

TCP全双工,具有发送缓冲区和接收缓冲区。

流量控制

连接管理机制

如何理解连接

理解 - TCP建立连接的三次握手

理解 - TCP断开连接的四次挥手

TCP四次挥手的状态变化

四次挥手的CLOSE_WAIT状态

四次挥手的TIME_WAIT状态

多进程,多线程,线程池版的基于TCP/IP协议的client-server

tcp_server.hpp

tcp_server.cc

tcp_client.cc

超时重传机制

滑动窗口

快重传

拥塞控制

延迟应答

捎带应答

TCP面向字节流

粘包问题

针对TCP面向字节流-粘包问题的自定义应用层协议的网络版本计算器server&&client

TCP异常情况

TCP/UDP对比

真·TCP/UDP对比

如何让UDP实现可靠传输

listen的第二个参数


计算机网络的层状结构

计算机网络是层状的,UDP/TCP协议是传输层协议。

我们使用的网络编程系统调用就是传输层提供的接口。例如accept, connect, listen, socket...

网络传输时,向下要封装报头,向上要解包,也就是去掉报头。发送方需要将应用层数据包逐层向下,添加每层的报头进行封装。接收方通过网络接收到对方传来的报文时,需要逐层向上去掉报头,进行解包,提取出最终的有效载荷。也就是网络发送方真正要传输的数据。

几乎任何协议,都要首先解决两个问题:1. 如何分离(将报头和有效载荷拆分开,接收方需要做的)和如何封装(发送方做的,添加报头)2. 如何向上交付(有效载荷拆分出来之后,交付给上一层)

套接字 = IP + 端口号。IP是网络层协议报头包含的字段,标识着网络传输时应该将数据传输给哪个主机。端口号是传输层协议报头包含的字段,对应着传输层报文中的有效载荷应该交付给该主机上的哪个进程。这样对应进程收到传输层的有效载荷之后,就可以根据应用层协议,将应用层报文中的有效载荷提取出来。

这块其实是网络基础1的内容,之前没写博客... 这里简单记录下。

UDP协议

UDP报文格式

1. 源端口号,目的端口号标明了此UDP报文是哪个进程发出的,发送给哪个进程。
2. 如何解包(分离):UDP采用固定长度报头,接收方将报文前8字节提取出,剩下的就是有效载荷。
3. 如何向上交付:接收方的OS的传输层收到UDP报文之后,16位目的端口号标明了对应进程。(该进程bind了端口号,在内核中,存储诸如port : PCB指针这样的KV类型,就可以通过端口号找到对应的进程)
4. 承接第三点,这也是为什么在应用层编写UDP代码时,定义端口号时,喜欢定义为uint16_t,正是因为传输层协议使用的端口号为16位的。
5. UDP如何提取到整个完整报文:16位UDP长度字段(???????这块好像涉及到UDP报文的存储方式了... 真的需要提取吗....(大概率是需要的...) TCP仅包含报头长度,有什么影响吗???????)

理解UDP/TCP报文的本质

1. UDP/TCP报头在操作系统中本质就是一个位段类型。
2. OS中会有很多UDP报文,TCP报文,那么,OS需要管理这些报文,即先描述,再组织。所以报文在内核中并非仅位段 + 有效载荷。还会有其他字段。

UDP的特点

UDP传输过程类似于寄信。

无连接: 知道对端的IP和端口号就可以直接进行传输, 不需要建立连接;(sendto)

不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该数据段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;

面向数据报: 不能够灵活的控制读写数据的次数和数量;  :  应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;   用UDP传输100个字节的数据: 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;  发送端的sendto和接收端的recvfrom次数是一样的。

UDP的缓冲区

UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;

UDP具有接收缓冲区,但是因为UDP不可靠,没有任何传输控制行为。故这个接收缓冲区无法保证接收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了,再到达的UDP数据就会被丢弃。(提醒一下,接收缓冲区中存储的是UDP报文中去掉报头之后的有效载荷)

sendto/recvfrom/send/recv/write/read IO类接口

我们调用UDP的sendto/recvfrom和TCP的recv/send时,表面上是网络发送和网络接收函数,实质上,它们只是拷贝函数,将应用层缓冲区的数据拷贝到发送缓冲区,将接收缓冲区中的数据拷贝到应用层缓冲区中。(特别是对于TCP而言)(注意,UDP没有发送缓冲区,所以为虚线,若TCP则为实线。)

将数据拷贝到发送缓冲区之后,什么时候进行网络发送,发多少,出错了怎么办,这些都是由传输层协议决定的。缓冲区也是传输层提供的。

UDP是全双工的

UDP没有发送缓冲区,有接收缓冲区,数据在网络中的发送和接收互不影响,可以同时进行,因此为全双工的。UDP的socket既能读,也能写。

UDP注意事项

我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部8字节). 然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;

UDP协议,实现简单聊天室(服务端+客户端)

udpserver.hpp

#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_

#include "log.hpp"
#include <string>
#include <cstring>
#include <unordered_map>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 基于UDP协议的服务端
class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip), _sock(-1)
    {
    }
    void initServer()
    {
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 1.套接字类型:网络套接字(不同主机间通信) 2.面向数据报还是面向字节流:UDP面向数据报
        // SOCK_DGRAM支持数据报(固定最大长度的无连接、不可靠消息)。
        if (_sock < 0)
        {
            // 创建套接字失败?
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2.bind : 将用户设置的ip和port在内核中和我们当前的进程强关联
        struct sockaddr_in local; // 传给bind的第二个参数,存储ip和port的信息。
        local.sin_family = AF_INET;
        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!-> 故需要转换为网络字节序
        local.sin_port = htons(_port); // host->network l 16
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域,4字节
        // INADDR_ANY:让服务器在工作过程中,可以从本机的任意IP中获取数据(一个服务器可能不止一个ip,(这块有些模糊)
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        // 点分十进制字符串风格的IP地址 <-> 4字节整数   4字节主机序列 <-> 网络序列   inet_addr可完成上述工作
        if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0) // !!!
        {
            logMessage(FATAL, "bind : %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server %s...", strerror(errno));
    }
    void start()
    {
        // 作为一款网络服务器,永远不退出的!-> 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        char buff_message[1024]; // 存储从client发来的数据
        for (;;)
        {
            struct sockaddr_in peer;     // 输出型参数
            socklen_t len = sizeof peer; // 输出+输入型参数
            memset(&peer, 0, len);

            // 读取client发来的数据
            ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len); // 哪个ip/port给你发的
            // receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
            buff_message[sz] = 0;
            uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来,转换为主机序列!哈哈哈
            std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
            char key[128];
            snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port);
            logMessage(NORMAL, "[%s:%d] clinet worked", cli_ip.c_str(), cli_port);
            // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message);    // 可有可无...服务端显示一下客户端发来了什么
            if (_map.find(key) == _map.end())
            {
                _map.insert({key, peer});
                logMessage(NORMAL, "[%s:%d] client joined", cli_ip.c_str(), cli_port);
            }
            // if (sz > 0)
            // {
            //     // 从client获取到了非空数据,client端的ip/port信息存储在peer中。
            //     buff_message[sz] = 0;
            //     uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来,转换为主机序列!哈哈哈
            //     std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
            //     snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port);
            //     logMessage(NORMAL, "[%s:%d] clinet worked", cli_ip.c_str(), cli_port);
            //     // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message);    // 可有可无...服务端显示一下客户端发来了什么
            //     if(_map.find(key) == _map.end())
            //     {
            //         _map.insert({key, peer});
            //         logMessage(NORMAL, "[%s:%d] client joined", cli_ip.c_str(), cli_port);
            //     }
            // }
            // else
            // {
            //     // 这里的逻辑是:如果client端最初发送空数据,则不加入群聊。
            //     buff_message[0] = 0;
            // }
            // 群聊服务端,此时需要给所有群聊中的client,发送某client发来的数据
            for (auto &iter : _map)
            {
                // if(iter.second.sin_port != peer.sin_port)
                std::string sendMessage(key);
                sendMessage += "# ";
                sendMessage += buff_message;
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&iter.second, sizeof iter.second);
            }
        }
    }
    ~UdpServer()
    {
        close(_sock);
    }

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;  // 端口号
    std::string _ip; // ip
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _map; // [ip:port] | struct
};

#endif

udp_server.cc

#include "udp_server.hpp"
#include <iostream>
#include <memory>
#include <cstdlib>

static void Usage(const char *proc)
{
    std::cout << "\nUsage: " << proc << " port\n"
              << std::endl;
}

// 格式:./udp_server 8080
// 疑问: 为什么不需要传ip?
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

udp_client.cc

#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
static void Usage(char *proc)
{
    std::cout << "\nUsage : " << proc << " server_ip server_port\n"
              << std::endl;
}

void *in(void *args)
{
    // 接收server端发送的数据
    logMessage(NORMAL, "接收线程已启动");
    int sock = *(int *)args; // 套接字
    char message_svr[1024];  // 缓冲区
    while (true)
    {
        struct sockaddr_in server;
        socklen_t len = sizeof server;
        // 一直都在等待UDP报文,进行接收
        ssize_t sz = recvfrom(sock, message_svr, sizeof message_svr - 1, 0, (struct sockaddr *)&server, &len);
        if (sz > 0)
        {
            message_svr[sz] = 0;
            std::cout << message_svr << std::endl;
        }
    }
}

struct ThreadData
{
    ThreadData(int sock, const std::string &ser_ip, uint16_t ser_port)
        : _sock(sock), _server_ip(ser_ip), _server_port(ser_port)
    {
    }
    int _sock;
    std::string _server_ip;
    uint16_t _server_port;
};

void *out(void *args)
{
    // 发送client端发送的数据
    logMessage(NORMAL, "发送线程已启动");
    ThreadData *td = (ThreadData *)args;
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_port = htons(td->_server_port);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(td->_server_ip.c_str());
    std::string message;
    while (true)
    {
        // std::cout << "client# ";
        std::cerr << "client# ";  // 为了不让这个输出信息也输出到管道中!和服务端发来的进行区分!否则会出现乱码现象。
        // fflush(stdout);
        std::getline(std::cin, message);

        sendto(td->_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        logMessage(FATAL, "socket : %d:%s", errno, strerror(errno));
        exit(2);
    }
    pthread_t itid, otid;
    pthread_create(&itid, nullptr, in, (void *)&sock);
    struct ThreadData td(sock, argv[1], atoi(argv[2]));
    pthread_create(&otid, nullptr, out, (void*)&td);

    pthread_join(itid, nullptr);
    pthread_join(otid, nullptr);
    return 0;
}

上方

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值