Linux Socket编程

UDP编程

常用接口

socket () - 创建套接字

头文件:#include <sys/socket.h>

cpp 复制代码
int socket(int domain, int type, int protocol);
  • 功能:生成一个全新套接字,返回套接字对应的文件描述符,是所有 TCP/UDP 网络程序的起始接口,客户端、服务端程序都需要率先调用该函数。
  • 参数说明
    • domain:协议族,常用AF_INET(IPv4 互联网通信)、AF_INET6(IPv6 通信)、AF_UNIX(本机进程本地通信);
    • type:套接字类型,SOCK_STREAM对应 TCP 流式套接字、SOCK_DGRAM对应 UDP 数据报套接字;
    • protocol:一般赋值为 0,操作系统会根据 domain 与 type 自动匹配对应的底层通信协议。
  • 返回值 :创建成功返回非负的文件描述符;创建失败返回-1,并且自动设置全局错误标识errno
  • 底层原理 socket 函数返回的文件描述符会关联内核缓冲区,应用向该缓冲区写入的数据,由操作系统完成协议封装后经由网卡发送至远端主机,因此 socket 编程本质是跨主机的进程间通信。

bind () - 绑定套接字

头文件:#include <sys/types.h>#include <sys/socket.h>

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将创建的套接字与本机特定 IP 地址和端口号绑定,为服务器程序确立固定的通信地址标识,让客户端能够通过该地址发起连接请求,是 TCP/UDP 服务器初始化的关键步骤之一。

  • 参数说明

    • sockfd:由socket()函数返回的套接字文件描述符,唯一标识要绑定的套接字;
    • addr:指向通用地址结构体sockaddr的指针,实际传入时通常使用struct sockaddr_in(IPv4)或struct sockaddr_in6(IPv6)结构体并强制类型转换,其中包含要绑定的 IP 地址和端口号(端口号需通过htons转换为网络字节序);
    • addrlen:地址结构体的字节长度,一般通过sizeof运算符获取实际传入的地址结构体大小。
  • 返回值 :绑定成功返回0;绑定失败返回-1,并设置全局错误标识errno,常见错误包括端口被占用(EADDRINUSE)、地址不可用(EADDRNOTAVAIL)等。

  • 底层原理 bind 函数本质是向操作系统内核注册套接字的网络标识,内核会记录该套接字与特定 IP + 端口的映射关系,后续网络数据包到达时,内核可通过目标地址快速定位到对应的套接字并交付数据。对于服务器程序,绑定固定端口是必要操作;而客户端程序通常无需显式调用 bind,由内核自动分配临时端口。

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

cpp 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 功能:向已建立连接的套接字缓冲区写入数据,由内核完成后续封装与网络发送;write 是 POSIX 通用写接口,在 socket 场景下作用和 flags=0 的 send 完全一致。
  • 参数
    • sockfd:通信套接字文件描述符,TCP 使用 accept 返回的新 fd、connect 后的客户端 fd。
    • buf:指向待发送数据缓冲区的指针,存放要发送的二进制数据。
    • len:期望发送的数据字节长度。
    • flags:控制选项,日常编程默认填 0,使用常规阻塞发送逻辑。
  • 返回值:成功返回实际发送出去的字节数;失败返回 - 1 并设置 errno。

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

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 功能:从套接字内核接收缓冲区读取远端发来的数据;read 等价于 flags=0 的 recv,是通用文件读接口。
  • 参数
    • sockfd:用来接收数据的通信套接字描述符。
    • buf:应用层缓冲区,用来存放读到的数据。
    • len:缓冲区最大可读取字节长度。
    • flags:接收控制标识,常规业务统一传 0。
  • 返回值: 成功返回本次实际读到的字节数; 返回 0 代表对端主动关闭 TCP 连接; 读取出错返回 - 1 并填充 errno 错误码。

C++封装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_in
        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;
};

客户端和服务器构建

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;
}

测试:

TCP编程

常用接口

listen() - 连接

cpp 复制代码
int listen(int sockfd, int backlog);

作用 :将套接字设置为被动监听模式,等待客户端连接。

参数

  • sockfd:已绑定的套接字
  • backlog:内核等待连接的队列最大长度
  • 注意:仅服务器调用,listen 不阻塞,只是开启监听状态。

accept() - 接受连接

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

作用 :从监听队列中取出一个客户端连接,创建新的通信套接字

参数

  • sockfd:监听套接字
  • addr:输出参数,存储客户端的 IP 和端口
  • addrlen:输出参数,地址结构体长度关键
  1. 默认阻塞等待,直到有客户端连接
  2. 返回值是新的套接字,专门用于和该客户端收发数据
  3. 原监听套接字继续等待新连接

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

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

作用 :客户端主动向服务器发起 TCP 连接(三次握手)

参数

  • sockfd:客户端套接字
  • addr:目标服务器的 IP 和端口
  • addrlen:结构体长度注意:仅客户端调用,连接失败会直接返回 - 1。

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;
};

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;
};

客户端和服务器构建

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();
}