【网络】TCP与UDP协议使用指南,Socket编程实现Echo服务

文章目录

本文完整项目代码已提交在我的github仓库中

一、TCP协议和UDP协议的特点

我们了解了TCP/IP协议栈,就明白:在应用层编写网络程序,必须要和传输层交流。传输层属于操作系统内核,我们必须调用相关的系统调用来进行网络通信。

TCP协议和UPD协议是传输层两种最主要的协议:

  • TCP(Transmission Control Protocol 传输控制协议),特点是:
    • 有连接:通信前必须先和对方建立连接,通信结束必须断开连接
    • 可靠传输:它能保证数据不丢包、不乱序、不重复
    • 面向字节流:它把数据当成一串连续的字节流来传输,需要应用层自己判断数据的边界。
  • UDP(User Datagram Protocol 用户数据协议),特点是:
    • 无连接:不需要提前建立连接,直接把数据打包发出去就行。
    • 不可靠传输:数据发出去就不管了,它不保证数据一定能送到,也不保证顺序、不保证不重复。
    • 面向数据报:UDP每次发送的数据是一个完整的"数据报",接收方收到的一定是一个完整的包。

二、网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件如此,网络数据流也是如此。

TCP/IP协议规定:网络中传递的数据必须是大端!

因此,不管主机是大端还是小端机器,它向网络流中发送数据必须是大端模式的;它从网络流中读取数据也是大端模式的。

例如,如果发送方是小端机器,则它必须先将数据转成大端再发送,反之直接发送即可。

为了使网络程序具有良好的可移植性,同样的程序在大小端机器上都能运行,可以使用以下库函数做网络字节序和主机字节序的转换:

cpp 复制代码
#include <arpa/inet.h>
// 当前主机序列转为网络序列
uint32_t htonl(uint32_t hostlong); 
uint16_t htons(uint16_t hostshort);

// 网络序列转为当前主机序列
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

// uint32_t是32位无符号整型类型,uint16_t是16位无符号整型类型

三、Socket编程常用接口

1. 创建套接字与绑定IP+端口

这是UDP和TCP都必须有的两步,必须先创建好一个套接字并且绑定IP和端口号,才能进行通信。

  • socket函数,创建socket文件描述符(向操作系统申请一个套接字):

    第一个参数表示你要用哪种网络类型。AF_INET代表IPv4 网络,AF_INET6代表IPv6网络;

    第二个参数表示你要用哪种传输方式。SOCK_STREAM代表TCP。SOCK_DGRAM代表UDP;

    第三个参数表示协议号,直接写0,系统会自动匹配;

    如果创建成功,函数返回创建好的socket文件描述符,否则返回-1。

  • bind函数,向一个已有的socket绑定 IP + 端口,这个函数通常由服务端执行,而客户端不需要:

    第一个参数是刚才创建好的空socket文件描述符;

    第二个参数是一个结构体,这个结构体需要包含IP和端口号等信息;第三个参数是这个结构体的大小。实际填写这个结构时,我们需要这样写:

cpp 复制代码
// 假设_socketfd _port已有
struct sockaddr_in local;
bzero(&local, sizeof(local)); //清空local
local.sin_family = AF_INET; //表明网络类型为IPv4
local.sin_port = htons(_port); // 把端口号转为网络序列,记录到local中
local.sin_addr.s_addr = INADDR_ANY; // 表明绑定本主机上所有网卡的ip地址

// 类型强转,两种结构体内存布局完全一样
bind(_socketfd, (struct sockaddr*)local, sizeof(local)); 

服务端,需要我们手动调用bind函数完成绑定。

但在客户端,需要注意的是:

客户端也一定需要有自己的IP和Port信息
但是,客户端不需要显示地bind自己的IP和Port!

为什么不让客户端显示bind?因为bind port可能和客户端机器上已被使用的port出现冲突!客户端port只需要具有唯一性即可,具体是几,不重要。客户端一般会采用随机端口的方式,由OS自主选择端口自动绑定!
UDP和TCP客户端首次发送数据的时候,OS底层会隐式自动帮你进行获取随机端口,然后自动完成绑定IP和Port工作。

2. UDP收发数据

由于UDP是无连接、不可靠传输,完成上述两步直接收发数据即可。

  • recvfrom函数,UDP从网络中接收数据,获取客户端地址:

    sockfd:文件描述符;

    buf:接收数据的缓冲区;

    len:buf缓冲区大小;

    flags:填0即可,表示阻塞读取数据;

    src_addr:输出型参数,函数调用结束后,它会自动填入数据发送方的 IP + 端口号。我们也需用struct sockaddr_in类型变量强转接收。

    addrlen:src_addr的大小

    返回值:读取成功返回读取到的字节数,读取失败返回-1

  • sendto函数,UDP发送数据,必须写明目标IP和端口号。dest_addr参数表明要向谁发送数据,其他参数和recvfrom道理类似。

由于UDP是无连接的,只用read、write函数无法得知对方的IP和端口号。所以UDP中必须使用recvfrom和sendto函数,每次通信都要带着IP+Port信息。

3. TCP建立连接

UDP通信中,创建套接字、绑定ip和port,就可以直接收发数据了,不需要提前建立连接。

但是,TCP通信前,必须先和对方建立连接。这代表TCP在收发数据之前,必须有服务端监听连接请求、客户端发起连接请求的过程!

  • listen函数,TCP服务端开始持续监听是否有发起连接请求:

    第一个参数是服务端自己的套接字,第二个参数代表同时发起连接请求的链接队列长度(一般为16或32)

    函数调用成功返回0,失败返回-1

  • accept函数,TCP服务端用于接收一个连接请求:

    第一个参数是已经处在listen状态的服务端套接字,第二个输出型参数会记录发起连接请求的客户端的IP+Port信息!

    值得注意的是,函数调用失败返回-1。函数成功返回一个新的套接字文件描述符,这个fd用于真正和刚才发起连接的客户端进行通信。而原本accept参数的sockfd,可以认为只是一个专门用来监听的套接字!

  • connect函数,TCP客户端用于发起连接请求:

    第一个参数是客户端自己的套接字,第二个参数存放服务端的IP+Port,告诉程序要向谁发起连接请求。

走到这里,TCP已经建立了连接,通信双方对象固定,直接read/write函数,或recv/send函数,即可收发数据!

四、UDP实现Echo服务测试

我们使用UDP协议实现一个echo服务:客户端向服务端发送一串内容,服务端收到后什么都不做,再返回给客户端。

服务端:

cpp 复制代码
// Echo_Server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Logger.hpp"

const static int default_fd = -1;
const static int default_port = 8888;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:

    // 服务端端口号,必须自己设定好。客户端必须提前知道服务器端口号,才能访问
    UdpServer(uint16_t port = default_port)
        : _port(port),
          _sockfd(default_fd)
    {}

    ~UdpServer()
    {
        close(_sockfd);
    }

    void Init()
    {
        // 第一步: 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4, UDP通信
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

        // 第二步: 填充IP和端口号信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清空local
        local.sin_family = AF_INET; // IPv4
        local.sin_port = htons(_port); // 写入端口号
        local.sin_addr.s_addr = INADDR_ANY; // 当前机器上任意IP地址都计入

        // 第三步:bind socket信息
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 强转类型
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
    }

    void Start()
    {
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            // 读取数据
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                // 网络序列转为主机序列
                uint16_t client_port = ntohs(peer.sin_port);

                // inet_ntoa函数,能将sin_addr类型的ip地址,转为xx.xx.xx.xx风格字符串,序列也会自动转换
                std::string client_ip = inet_ntoa(peer.sin_addr); 

                std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]# ";

                // 客户端发来的数据
                inbuffer[n] = 0;
                LOG(LogLevel::INFO) << client_address << inbuffer;
                
                std::string echo_string = "server echo# ";
                echo_string += inbuffer;

                // 向客户端回显数据, peer中记录了客户端是谁
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
        }
    }

private:
    int _sockfd;
    uint16_t _port;  // 服务端自己设置好,port必须是固定的!因为一定是客户端发起第一次通信,客户端必须提前知道服务端是谁
};
cpp 复制代码
// Echo_Server_main.cc
#include"Echo_Server.hpp"
#include "Logger.hpp"
#include <memory>

// 执行程序命令行参数为 ./server_udp port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        exit(USAGE_ERR);
    }

    USE_CONSOLE_LOG_STRATEGY();

    uint16_t server_port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);

    usvr->Init();
    usvr->Start();

    return 0;
}
cpp 复制代码
// Echo_Client_main.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Logger.hpp"
        
// 客户端必须提前知道服务端的IP和port!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        exit(1);
    }
    
    USE_CONSOLE_LOG_STRATEGY();

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "create socket error";
        exit(2);
    }

    // 2. 构建服务端端socket信息 
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port); 
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    while(true)
    {
        std::string message;
        // 获取用户输入
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        // clinet 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n > 0)
        {
            char inbuffer[1024] = {0};
            struct sockaddr_in tmp;
            socklen_t len = sizeof(tmp);
            ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&tmp, &len);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer << std::endl;
            }
        }
    }


    return 0;
}

运行:

五、TCP实现Echo服务测试

为了便于后续使用,我们先自己封装好网络地址相关接口:

cpp 复制代码
// InetAddr.hpp 对网络地址进程描述
#pragma once
#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <sys/socket.h>

class InetAddr
{
public:
    InetAddr() = default;

    InetAddr(const struct sockaddr_in& address) : _address(address), _len(sizeof(address))
    {
        _ip = inet_ntoa(_address.sin_addr);
        _port = ntohs(_address.sin_port);
    }
    
    InetAddr(uint16_t port, const std::string& ip = "0.0.0.0") : _ip(ip), _port(port)
    {
        // 自动构造struct sockaddr_in 类型网络地址
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);
        _address.sin_addr.s_addr = inet_addr(_ip.c_str());
        _len = sizeof(_address);
    }

    bool operator==(const InetAddr& addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }

    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    struct sockaddr* GetNetAddress()
    {
        return (struct sockaddr*)&_address;
    }

    socklen_t Len()
    {
        return _len;
    }

    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _address;
    socklen_t _len;
    std::string _ip;
    uint16_t _port;
};

处理一个通信任务,可以多进程、多线程、线程池...这里以线程池为例,使用我以前自己实现的单例线程池。

代码如下:

cpp 复制代码
// Echo_Server.hpp
#pragma once
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
#include <cstdlib>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

using task_t = std::function<void()>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    FORK_ERR
};

static const int gbacklog = 16;
static const uint16_t gport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _port(port)
    {
    }

    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP, 使用SOCK_STREAM选项
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        // 2. 填充本地socket信息
        InetAddr local(_port); // 任意地址bind

        // 3. bind
        int n = bind(_listensockfd, local.GetNetAddress(), local.Len());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success";

        // 4. TCP是面向连接的,TCP服务器必须先要处于listen状态。
        n = listen(_listensockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen socket success";
    }

    void Start()
    {
        while (1)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 5. 获取连接
            int sockfd = accept(_listensockfd, (struct sockaddr*)&clientaddr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::INFO) << "accept success, sockfd: " << sockfd;

            // 处理通信任务:多进程、多线程、线程池,这里使用线程池
            // 将通信任务放入线程池
            InetAddr clientaddress(clientaddr);
            ThreadPool<task_t>::Instance()->Enqueue([this, sockfd, clientaddress]() -> void
                                                    { this->serviceIO(sockfd, clientaddress); });
        }
    }

    void serviceIO(int sockfd, InetAddr address)
    {
        // 长连接,长服务
        while (1)
        {
            char inbuffer[1024] = {0};
            // 读消息
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

                // 写回消息
                std::string echo_string = "server echo# ";
                echo_string += inbuffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
                break;
            }
            else
            {
                LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
                break;
            }
        }
        close(sockfd);
    }

    ~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    // 不需要显示包含ip
    int _listensockfd; // 专门用于监听的套接字
};
cpp 复制代码
// Echo_Client.cc
#include "InetAddr.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cerr << "./client_tcp server_ip server_port" << std::endl;
        exit(1);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建tcp套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // 客户端不需要显示bind

    // 2. 向服务端发起连接
    InetAddr serveraddress(server_port, server_ip);
    int n = connect(sockfd, (struct sockaddr*)serveraddress.GetNetAddress(), serveraddress.Len());
    if (n < 0)
    {
        std::cerr << "connect to " << serveraddress.ToString() << " failed!" << std::endl;
        exit(3);
    }
    std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;

    // 3. 通信
    while (1)
    {
        std::string line;
        std::cout << "please Enter# ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "read end of file!" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error!" << std::endl;
            break;
        }
    }

    return 0;
}
cpp 复制代码
// Echo_Server_main.cc
#include "Echo_Server.hpp"
#include "Logger.hpp"
#include <memory>

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "./server_tcp port" << std::endl;
        exit(USAGE_ERR);
    }
    USE_CONSOLE_LOG_STRATEGY();
    uint16_t server_port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port);
    tsvr->InitServer();
    tsvr->Start();

    return 0;
}

效果演示:

本篇完,感谢阅读。

相关推荐
say_fall2 小时前
校招必看:八大排序算法原理、复杂度与高频面试题
数据结构·c++·算法·排序算法
草莓熊Lotso2 小时前
LangChain从入门到精通:环境搭建→核心能力→LCEL链式编程全实战
android·java·linux·服务器·langchain
Hanniel2 小时前
C++枚举新手入门教程
c++
GanGanGanGan_10 小时前
RustDesk 安装指南 — Rocky Linux 9 + XFCE X11
linux·运维·centos
风落无尘10 小时前
《智能重生:从垃圾堆到AI工程师》——第五章 代码与灵魂
服务器·网络·人工智能
许长安11 小时前
RPC 同步调用基本使用方法:基于官方 RouteGuide 示例
c++·经验分享·笔记·rpc
kyriewen1111 小时前
WebAssembly:前端界的“外挂”,让C++代码在浏览器里跑起来
开发语言·前端·javascript·c++·单元测试·ecmascript
S1998_1997111609•X13 小时前
论当今社会主义与人文关怀人格思想下的恶意仿生注入污染蜜罐描述进行函数值非法侵入爬虫的咼忄乂癿〇仺⺋.
数据库·网络协议·百度·ssh·开闭原则