《Linux网络编程》2.Socket编程(UDP/TCP)

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》

《个人在线OJ平台》《Linux网络编程》


🌸Yupureki🌸的简介:


目录

[1. UDP编程](#1. UDP编程)

[1.1 常用接口](#1.1 常用接口)

[1.1.1 socket() -- 创建套接字](#1.1.1 socket() – 创建套接字)

[1.1.2 bind() -- 绑定地址和端口](#1.1.2 bind() – 绑定地址和端口)

[1.1.3 send() / write() - 数据发送](#1.1.3 send() / write() - 数据发送)

[1.1.4 recv() / read()- 数据接收](#1.1.4 recv() / read()- 数据接收)

[1.2 C++封装UDPServer](#1.2 C++封装UDPServer)

[1.3 客户端和服务器构建](#1.3 客户端和服务器构建)

[2. TCP编程](#2. TCP编程)

[2.1 常用接口](#2.1 常用接口)

[2.2.1 listen() -- 监听连接](#2.2.1 listen() – 监听连接)

[2.2.2 accept() -- 接受连接](#2.2.2 accept() – 接受连接)

[2.2.3 connect() -- 发起连接(客户端使用)](#2.2.3 connect() – 发起连接(客户端使用))

[2.2 C++封装InetAddr](#2.2 C++封装InetAddr)

[2.3 C++封装TCPServer](#2.3 C++封装TCPServer)

[2.4 客户端和服务器构建](#2.4 客户端和服务器构建)


1. UDP编程

UDP(User Datagram Protocol)是无连接的传输层协议

1.1 常用接口

1.1.1 socket() -- 创建套接字

cpp 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 功能:创建一个新的套接字,返回文件描述符。

  • 参数

    • domain:协议族,常用 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信)。

    • type:套接字类型,常用 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。

    • protocol:通常设为 0,让系统自动选择协议(TCP 或 UDP)。

  • 返回值 :成功返回非负文件描述符 ,失败返回 -1 并设置 errno

socket会返回一个文件描述符 ,这个文件描述符指向一个文件缓冲区 ,我们往这个缓冲区输入的数据,会被系统发送到远端主机上。因此socket编程本质就是进程间通信

1.1.2 bind() -- 绑定地址和端口

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能 :将套接字与本地 IP 地址和端口绑定。服务端必须调用,客户端通常不调用(由系统自动分配)。

  • 参数

    • sockfdsocket() 返回的描述符。

    • addr:指向 sockaddr_in(IPv4)或 sockaddr_in6 结构的指针,包含 IP 和端口。

    • addrlen:地址结构的长度。

  • 返回值:0 成功,-1 失败。

sockaddr 是一个通用的地址结构体, 它本身不直接使用,而是作为 bind()connect()accept() 等函数的参数类型,允许这些函数接收不同协议族的地址结构。

但使用的时候一般是sockaddr_in

cpp 复制代码
struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IPv4 地址(网络字节序)
    char           sin_zero[8];  // 填充,使大小与 sockaddr 一致
};
  • 大小:16 字节(与 sockaddr 相同,便于类型转换)。

  • 使用时先填充 sockaddr_in,然后强制转换(struct sockaddr*) 传递给函数。

1.1.3 send() / write() - 数据发送

cpp 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 发送数据,flags 通常为 0。成功返回发送的字节数,失败返回 -1。

1.1.4 recv() / read()- 数据接收

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 接收数据,成功返回接收的字节数(0 表示对方关闭连接),失败返回 -1。

1.2 C++封装UDPServer

我们用C++封装Socket接口,实现一个简易的UdpServer

cpp 复制代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <iostream>

#define DEFAULT_PORT 8080
#define MAXNUM 1024

class UdpServer
{
public:
    UdpServer(uint16_t port = DEFAULT_PORT)//指定本地端口号
    :_port(port)
    {}
    void init()//初始化->1.创建套接字 2.绑定套接字
    {
        //创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
            exit(1);

        //初始化sockaddr_i
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        //绑定套接字
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n != 0)
            exit(1);
    }
    void start()//服务器启动
    {
        char buffer[MAXNUM];
        //客户端的信息
        struct sockaddr_in peer;
        
        socklen_t len = 0;
        while(1)
        {
            char buffer[MAXNUM];
            //接受数据
            int n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout<<buffer<<std::endl;
            }
        }
    }
private:
    uint16_t _port;
    int _sockfd = -1;
};

1.3 客户端和服务器构建

client.cpp:

  • 启动客户端时需要知道我们服务器的ip地址和端口号,这样才能知道给哪个服务器发送数据
  • 如果客户端和服务器都是本地启动的,客户端填的ip地址可以是127.0.0.1 ,表示本地环回地址,只在本地查找对应的端口号
  • 客户端和服务器都需要创建套接字并绑定
cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>

int main(int argc,char* argv[])//指定服务器的ip地址和端口号
{
    if(argc != 3)
    {
        std::cout<<"format:"<<__FILE__<<" ip port"<<std::endl;
        exit(1);
    }
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    //初始化sockaddr_in
    struct sockaddr_in sock;
    bzero(&sock,sizeof(sock));
    uint16_t port = atoi(argv[2]);
    sock.sin_family = AF_INET;
    sock.sin_port = htons(port);
    sock.sin_addr.s_addr = inet_addr(argv[1]);
    //启动客户端:本地键盘输入数据,传给服务器
    while(1)
    {
        std::cout<<"Enter->";
        std::string s;std::getline(std::cin,s);
        ssize_t n = sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&sock,sizeof(sock));
    }
    return 0;
}

server.cpp:

  • 服务器启动时需要指定本地端口号
cpp 复制代码
#include "server.hpp"

int main(int argv,char* argc[])
{
    if(argv != 2)
    {
        std::cout<<"format: "<<__FILE__<<" port"<<std::endl;
        exit(1);
    }
    uint16_t port = atoi(argc[1]);
    UdpServer udp(port);
    udp.init();
    udp.start();
    return 0;
}

测试:

2. TCP编程

2.1 常用接口

TCP编程使用的接口很多和UDP相似,也有部分不一样

  • 服务器绑定套接字后,需要监听(listen) ,当有客户端访问后,需要接受(accept)
  • socket 返回的文件描述符的意义不再是把缓冲区的数据发送给远端了 。而是作为监听套接字,启动监听状态 ,当发现有客户端连接后,会使用accpet函数 接受连接,此时返回的文件描述符才指向输入缓冲区
  • 客户端需要主动**连接(connect)**客户端

2.2.1 listen() -- 监听连接

cpp 复制代码
int listen(int sockfd, int backlog);
  • 功能:将套接字转为被动监听状态,等待客户端连接。

  • 参数

    • sockfd:已绑定的监听套接字

    • backlog:未完成连接队列的最大长度(通常设为 5~128)。

  • 返回值:0 成功,-1 失败。

2.2.2 accept() -- 接受连接

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能 :从监听套接字的完成连接队列中取出第一个连接,并创建一个新的套接字用于与客户端通信。

  • 参数

    • sockfd:监听套接字。

    • addr:输出参数,返回客户端的地址信息(可为 NULL)。

    • addrlen:输入输出参数,传入地址结构长度,返回实际长度。

  • 返回值 :成功返回新的连接套接字描述符,失败返回 -1。

2.2.3 connect() -- 发起连接(客户端使用)

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:客户端主动连接服务器。

  • 参数

    • sockfd:客户端套接字。

    • addr:服务器地址。

    • addrlen:地址长度。

  • 返回值:0 成功,-1 失败。

2.2 C++封装InetAddr

网络编程中,我们需要频繁进行网络字节序和主机字节序之间的转换,频繁使用sockaddr_in

因此为了方便,我们设计一个InetAddr类,其中包含代表网络字节序的sockaddr_in ,和本地字节序的ip地址和port端口号

cpp 复制代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <cstring>

class InetAddr{
public:
    InetAddr(struct sockaddr_in & addr)//网络字节序转主机字节序
    :_addr(addr)
    {
        _port = ntohs(addr.sin_port);
        char buffer[64];
        inet_ntop(AF_INET,&addr.sin_addr,buffer,sizeof(_addr));
        _ip = buffer;
    }
    InetAddr(std::string ip,uint16_t port)//主机字节序转网络字节序
    :_port(port),_ip(ip)
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
    }
    InetAddr(uint16_t port)//默认本地环回
    :_port(port),_ip("00.00.00.00")
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    const struct sockaddr_in& get_addr()
    {
        return _addr;
    }
    struct sockaddr_in* get_addr_ptr()
    {
        return &_addr;
    }
    std::string get_string()
    {
        return _ip + ":" + std::to_string(_port);
    }
    std::string get_ip()
    {
        return _ip;
    }
    uint16_t get_port()
    {
        return _port;
    }
private:
    uint16_t _port;
    std::string _ip;
    struct sockaddr_in _addr;
};

2.3 C++封装TCPServer

在封装TCPServer前,我们需要清楚一个概念:行使一个业务的服务器只能有一个,不能复制。因此我们设计一个类,这个类作为基类,不允许被复制,使得他的派生类也不能复制。我们把这个类放进comm.hpp。同时为了代码的正规性,我们设计统一的退出码

cpp 复制代码
#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "InetAddr.hpp"
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <string>

#define CONV(x) ((struct sockaddr*)(&x))
#define DEFAULT_BACKLOG 8
#define MAXNUM 1024

enum ExitCode{
    SOCKET = 0,
    BIND,
    LISTEN,
    ACCEPT,
    FORMAT,
    CONNECT,
};

class nocopy{
public:
    nocopy()
    {}
    ~nocopy()
    {}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

server.hpp:

这个TCPServer我们期望他实现:

  1. 初始化和绑定socket套接字
  2. 设置监听状态
  3. 接受客户端的连接请求
  4. 连接成功后,使用回调函数,实现对应的业务
cpp 复制代码
#pragma once

#include "com.hpp"

class TcpServer : public nocopy
{
private:
    using func_t = void(*)(int ,InetAddr&, bool&);
public:
    TcpServer(uint16_t port,func_t func)
    :_port(port),_func(func)
    {}
    void init()
    {
        _listensockfd = socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd < 0)
            exit(ExitCode::SOCKET);
        InetAddr addr(_port);
        int n = bind(_listensockfd,CONV(addr.get_addr()),sizeof(struct sockaddr_in));
        if(n < 0)
            exit(ExitCode::BIND);
        n = listen(_listensockfd,DEFAULT_BACKLOG);
        if(n < 0)
            exit(ExitCode::LISTEN);
    }
    void run()
    {
        _isrunning = true;
        while(_isrunning)
        {
            struct sockaddr_in tmp;
            memset(&tmp,0,sizeof(tmp));
            socklen_t len = sizeof(tmp);
            int fd = accept(_listensockfd,CONV(tmp),&len);
            if(fd < 0)
                exit(ExitCode::ACCEPT);
            InetAddr peer(tmp);
            _func(fd,peer,_isrunning);//回调函数
        }
    }
private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning = false;
    func_t _func;
};

2.4 客户端和服务器构建

client.cpp:

  • 客户端需要主动连接服务器(未连接成功前,不可发送数据)
cpp 复制代码
#include "com.hpp"

int main(int argv,char* argc[])
{
    if(argv != 3)
    {
        std::cout<<"format: "<<"./"<<__FILE__<<" ip port"<<std::endl;
        exit(ExitCode::FORMAT);
    }
    InetAddr addr(argc[1],atoi(argc[2]));
    std::cout<<"user:"<<addr.get_string()<<std::endl;
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    socklen_t len = sizeof(struct sockaddr_in);
    ssize_t n = connect(sockfd,CONV(addr.get_addr()),len);
    if(n < 0)
        exit(ExitCode::CONNECT);
    while(1)
    {
        std::cout<<"Enter->";
        std::string s;std::cin>>s;
        int n = write(sockfd,s.c_str(),sizeof(s));
        if (n < 0)
            continue;
    }
    return 0;
}

server.cpp:

  • 服务器需要设置好回调函数
cpp 复制代码
#include "server.hpp"

void func(int fd,InetAddr &addr, bool &isrunning)
{
    while (isrunning)
    {
        char buffer[MAXNUM];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
        if (n < 0)
            continue;
        else if(n == 0)
            break;
        buffer[n] = '\0';
        std::cout<<"client say: "<<buffer<<std::endl;
    }
}

int main(int argv,char* argc[])
{
    if(argv != 2)
    {
        std::cout<<"format: "<<"./"<<__FILE__<<" port"<<std::endl;
        exit(ExitCode::FORMAT);
    }
    uint16_t port = atoi(argc[1]);
    TcpServer tsvr(port, func);
    tsvr.init();
    tsvr.run();
}

拓展:

我们目前实现的TCPServer有个很严重的问题:单线程/单进程服务器,只能接受一个客户端的请求并处理数据,当多个客户端同时访问时,无法连接。

因此实际业务中,TCPServer的构建都是多线程/多进程服务器

相关推荐
come112342 小时前
本地 Docker 容器的“网络端点状态异常”,如何快速修复
网络·docker·容器
tianyuanwo2 小时前
服务器OS多架构CI流水线架构设计:单架构隔离与多架构融合的权衡之道
服务器·ci/cd·架构
竹之却2 小时前
【Linux】内网穿透原理
linux·服务器·网络·frp·内网穿透·p2p·xtcp
薛定谔的码*2 小时前
双机热备份MSTP+VRRP+负载均衡
运维·网络·负载均衡
wbs_scy2 小时前
Linux 进程信号深度解析(下):信号的保存、阻塞与捕捉
运维·服务器
无巧不成书02182 小时前
Calibre 全系统安装配置教程|新手零门槛+命令行进阶+AI功能+内容服务器全解析
运维·服务器·人工智能·calibre·电子书管理·calibre命令行·电子书格式转换
欲盖弥彰13142 小时前
Linux设备驱动 -- TMP75AIDR驱动移植
linux·驱动开发·驱动·驱动移植·嵌入式linux驱动·tmp75aidr
AIminminHu2 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(二-1-(1):当你的CAD学会“想象”:图形技术与AI融合的三个层次)
c++·人工智能·几何·cad·几何内核·cad开发