1.介绍函数使用

1.创建套接字

int socket(int domain, int type, int protocol);

  • domain:指定协议族,如AF_INET(IPv4)或AF_INET6(IPv6)。

  • type:指定套接字类型,如SOCK_DGRAM(UDP)或SOCK_STREAM(TCP)。

  • protocol:通常设置为0,表示使用默认协议。

返回值:成功时返回一个套接字描述符,失败时返回-1。

2.绑定套接字到一个特定的IP地址和端口号

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • sockfd:由socket()返回的套接字描述符。

  • addr:指向sockaddr结构的指针,包含要绑定的地址信息。

  • addrlenaddr的长度。

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

3.从UDP套接字接收数据,获取发送方地址

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

  • sockfd:套接字描述符。

  • buf:用于存储接收到的数据的缓冲区。

  • len:缓冲区的大小。

  • flags:通常设置为0。

  • src_addr:指向sockaddr结构的指针,用于存储发送方的地址信息。

  • addrlen:指向socklen_t的指针,用于指定src_addr的长度。

4.向指定的地址发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

  • sockfd:套接字描述符。

  • buf:要发送的数据的缓冲区。

  • len:要发送的数据的长度。

  • flags:通常设置为0。

  • dest_addr:指向sockaddr结构的指针,包含目标地址信息。

  • addrlendest_addr的长度。

5.设置套接字选项

 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

  • sockfd:套接字描述符。

  • level:选项级别,如SOL_SOCKET

  • optname:选项名称,如SO_REUSEADDRSO_REUSEPORT

  • optval:指向选项值的指针。

  • optlen:选项值的长度。

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

6.将网络字节序的IP地址转换为点分十进制

char *inet_ntoa(struct in_addr in);

 hostshort:主机字节序的无符号短整数

返回值:网络字节序的无符号短整数。

7.将主机字节序的无符号整数转换为网络字节序

uint16_t htons(uint16_t hostshort);

  • hostshort:主机字节序的无符号短整数。

返回值:网络字节序的无符号短整数。

8.将网络字节序的无符号整数转换为主机字节序

uint16_t ntohs(uint16_t netshort);

  • netshort:网络字节序的无符号短整数。

netstate指令查看状态

etstat命令是一个功能强大的网络工具,用于查看和分析系统的网络状态。以下是netstat命令的一些常用选项和示例,帮助你查看网络连接状态:

常用选项

  1. -a:显示所有活动的网络连接,包括监听和非监听状态。

  2. -t:仅显示TCP协议相关的连接。

  3. -u:仅显示UDP协议相关的连接。

  4. -n:以数字形式显示地址和端口号,避免进行DNS解析。

  5. -l:仅显示监听状态的连接。

  6. -p:显示与网络连接相关联的进程ID和程序名称。

  7. -r:显示路由表信息。

  8. -i:显示网络接口统计信息。

类比理解:bind 就像「分配电话号码」​

假设你的程序是一个「电话」,网络通信需要两件事:

  1. 电话号码​(IP地址 + 端口):别人通过这个号码找到你。
  2. 电话机​(套接字):用来接听和拨打电话的工具。

bind 的作用
把你的「电话机」绑定到一个「电话号码」上。这样,别人(其他程序)才能通过这个号码联系到你。

2.实现socket通信

 1.服务端构造函数

要接收一个端口号,以及一个处理信息的函数,然后isrunning是一个判断执行状态,true表示运行中,就可以执行主体代码。

   UdpServer(uint16_t port,func_t func)
        :_sockfd(defaultfd),
        _port(port),
        _isrunning(false),
        _func(func)
        {}

 2.服务端初始化函数

创建端口号,设定为UDP模式(虽然第三参数为0,但是前两个参数就决定了模式是UDP的),setsockopt函数用来这个原因是,当服务器主动关闭连接的时候,套接字会进入TIME_WAIT状态,这个原因是,当服务器主动关闭连接的时候,套接字会进入TIME_WAIT状态,可以设置一下地址端口重用,不然就会打印出日志bind error信息。bero函数是吧给定的空间进行初始化为0,接着就是初始化协议族为四字节,然后端口号要转成网络字节序,然后设置套接字绑定的ip地址为INADDR_ANY,表示该套接字将监听所有可用的网络接口上的链接请求,INADDR_ANY是一个常量,,表示任何可用的网络接口,这样子设置是为了把所有的客户端信息接收了,客户端可能是各种网络的,所以设置这个可用全部接收,接着就是绑定套接字了,因为是在栈区上创建的,是在用户层面上,需要到内核态里,就需要bind函数来进行绑定,绑定后的套接字才是有用的。

 void Init()
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket error";
            exit(1);
        }
        int opt = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        LOG(LogLevel::INFO)<<"socket success, sockfd:"<<_sockfd;

        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)
        {
            LOG(LogLevel::FATAL)<<"bind error";
            exit(2);
        }
        LOG(LogLevel::INFO)<<"bind success,sockfd:"<<_sockfd;
    }

3.服务端开始函数

 进入这个函数就可以把运行状态设置为true,然后进入循环,设置buffer用来接收消息,创建sockaddr_in类型peer表示客户端的消息(如端口号和IP地址),因为recvfrom函数需要这个参数,这个函数是在UDP套接字接收数据,peer就是一个输出型参数,执行完后,可以从peer里知道是谁发的,就像信封,上面有发件人的信息,需要注意的是参数需要强转成sockaddr类型的,接收成功就进入判断,从peer结构体里可以得到客户端信息(访问成员变量),然后用inet_ntoa将网络字节序转成点分十进制,调用回调函数,把buffer传过去,得到的信息处理再返回,最后执行sendto函数把信息进行发送回去,告诉客户端已经接收到了这个信息。

在使用recvfrom函数时,需要将sockaddr_in强制转换为sockaddr类型,原因如下:

兼容性

  • sockaddr是一个通用的套接字地址结构体,用于支持多种协议族。sockaddr_in是internet环境下套接字的地址形式,专门用于IPv4地址。

  • 套接字函数如bindconnectrecvfrom等设计为使用通用的sockaddr结构体作为参数,以便能够支持不同的地址族。通过将sockaddr_in强制转换为sockaddr,可以确保这些函数能够接受IPv4地址信息,同时保持接口的一致性。

灵活性

  • 强制转换允许程序员在需要时传递特定的地址结构体,同时保持函数接口的通用性。这意味着,如果将来需要支持其他类型的地址(如IPv6地址sockaddr_in6),只需更改地址结构体的类型和相应的转换,而无需修改函数调用的代码。

统一性

  • sockaddrsockaddr_in的长度都是16个字节,因此可以互相转换。这种统一的内存布局使得在不同地址类型之间进行转换成为可能,而不会导致数据损坏或访问错误的内存区域。

void Start()
    {
        _isrunning=true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            
            std::cout << "服务器开始接收数据: " << std::endl;
            ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(s>0)
            {
                std::cout<<"进入判断"<<std::endl;
                int peer_port=ntohs(peer.sin_port);
                std::string peer_ip=inet_ntoa(peer.sin_addr);
                buffer[s]=0;
                std::cout << "buffer: " << buffer << std::endl;
                std::string result=_func(buffer);
                std::cout << "result: "  <<result << std::endl;
                sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);

            }

        }

    }



std::string defaulthandler(const std::string& message)
{
    std::cout<<"打印"<<std::endl;
    std::string hello="hello, ";
    hello+=message;
    return hello;
}

4.UdpServer.hpp文件总代码

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

using func_t =std::function<std::string(const std::string&)>;

const int defaultfd=-1;


class UdpServer
{
public:
    UdpServer(uint16_t port,func_t func)
        :_sockfd(defaultfd),
        _port(port),
        _isrunning(false),
        _func(func)
        {}

    void Init()
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket error";
            exit(1);
        }
        int opt = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        LOG(LogLevel::INFO)<<"socket success, sockfd:"<<_sockfd;

        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)
        {
            LOG(LogLevel::FATAL)<<"bind error";
            exit(2);
        }
        LOG(LogLevel::INFO)<<"bind success,sockfd:"<<_sockfd;
    }

    void Start()
    {
        _isrunning=true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            
            std::cout << "服务器开始接收数据: " << std::endl;
            ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(s>0)
            {
                std::cout<<"进入判断"<<std::endl;
                int peer_port=ntohs(peer.sin_port);
                std::string peer_ip=inet_ntoa(peer.sin_addr);
                buffer[s]=0;
                std::cout << "buffer: " << buffer << std::endl;
                std::string result=_func(buffer);
                std::cout << "result: "  <<result << std::endl;
                sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);

            }

        }

    }
    ~UdpServer()
    {

    }
private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
   
    func_t _func;

};

5.实例实现

main要写命令行参数,要接收端口号,不用IP地址是因为前面设置0地址为IP地址,就不需要接收这个参数了,创建一个port接收传入的端口号,这里的uint_16是一个重定义的变量,设置日志类型向控状态打印信息,std::unique_ptr是一个智能指针,用于管理动态分布的对象,它所管理的对象只能有一个所有者,std::make_unique是C++14引入的一个工厂函数,用于简化unique_ptr的创建过程,会调用new操作符来分配空间,返回一个unique_ptr对象。创建对象执行,这里不是.而是->是因为这是一个智能指针,所以需要箭头调用函数。

#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include <functional> 
std::string defaulthandler(const std::string& message)
{
    std::cout<<"打印"<<std::endl;
    std::string hello="hello, ";
    hello+=message;
    return hello;
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cerr<<"Usage:"<<argv[0]<<" port"<<std::endl;
        return 1;
    }

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

    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port, defaulthandler);
    usvr->Init();
    usvr->Start();
    return 0;
}

6.客户端实现

客服端的话需要传入地址和端口号,创建套接字,以及sockaddr_in类型变量,初始化创建的sockaddr_in结构体的值,进入循环创建string类型的input,要发送的信息,调用getline函数从cin输入流中获取信息放到input中,调用sendto函数向服务端发送消息,套接字,以及发送内容以及大小,还有向谁发送信息,以及信息大小这些参数,然后就是创建buffer来接收消息,向服务端发送消息,服务端反馈后,客服端接收,接收到就打印出接收的消息。

为什么需要传递sizeof(server)

  • 指定地址结构体的大小addrlen参数用于告诉sendto函数目标地址结构体的大小。这对于底层网络库正确解析地址信息至关重要。

  • 兼容性sockaddr是一个通用的地址结构体,而sockaddr_in是专门用于IPv4的地址结构体。通过传递sizeof(server),可以确保sendto函数知道它正在处理的是sockaddr_in结构体的大小。

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

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cerr<<"Usage:"<<argv[0]<<"server_ip server_port"<<std::endl;
        return 1;
    }

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

    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        std::cerr<<"socket error"<<std::endl;
        return 2;
    }

    struct sockaddr_in server;
    memset(&server,0,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 input;
        std::cout<<"Please Enter#";
        std::getline(std::cin,input);

        std::cout << "客户端发送数据: " << input.c_str() << std::endl;
        ssize_t n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));
        (void)n;

        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        ssize_t m=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(m>0)
        {
            buffer[m]=0;
            std::cout<<buffer<<std::endl;
        }
    }

    return 0;
}

Makefile文件

.PHONY:all
all:udpclient udpserver

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 
udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f udpclient udpserver

补充:客服端不需要绑定端口原因

你提出了一个非常好的问题!这里确实涉及到一个重要的网络编程概念。让我详细解释为什么这个客户端代码不需要bind

核心原因:客户端 vs 服务器的区别

服务器需要 bind

// 服务器必须绑定固定端口,让客户端能找到它
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

客户端通常不需要 bind

// 客户端让操作系统自动分配端口
// 不需要显式调用 bind()

操作系统自动处理机制

当你调用 sendto()时,如果socket还没有绑定端口,操作系统会自动完成绑定

// 第一次调用sendto时,操作系统会:
// 1. 自动选择一个可用端口号
// 2. 自动绑定到socket
// 3. 然后发送数据
ssize_t n = sendto(sockfd, input.c_str(), input.size(), 0, 
                   (struct sockaddr*)&server, sizeof(server));

验证自动绑定

你可以修改代码来验证这一点:

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

int main(int argc, char* argv[])
{
    // ... 前面的代码相同 ...
    
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 在发送前检查本地地址(此时应该未绑定)
    struct sockaddr_in local_addr;
    socklen_t addr_len = sizeof(local_addr);
    getsockname(sockfd, (struct sockaddr*)&local_addr, &addr_len);
    std::cout << "发送前端口: " << ntohs(local_addr.sin_port) << std::endl;
    
    // 发送数据(触发自动绑定)
    sendto(sockfd, input.c_str(), input.size(), 0, 
           (struct sockaddr*)&server, sizeof(server));
    
    // 发送后检查本地地址(此时应该已绑定)
    getsockname(sockfd, (struct sockaddr*)&local_addr, &addr_len);
    std::cout << "发送后端口: " << ntohs(local_addr.sin_port) << std::endl;
    
    // ... 后面的代码 ...
}

为什么这样设计?

1. 简化客户端编程

// 客户端不需要关心自己的端口号
// 操作系统自动管理,避免端口冲突

2. 支持多个客户端实例

// 可以同时运行多个客户端
// 每个都会获得不同的随机端口
./client 127.0.0.1 8080  // 端口 54321
./client 127.0.0.1 8080  // 端口 54322
./client 127.0.0.1 8080  // 端口 54323

3. 内核确实参与了

你提到的"设置到内核区域"确实发生了:

  • socket()调用在内核创建了socket结构

  • sendto()触发的自动绑定在内核完成端口分配

  • 所有网络操作最终都要通过内核

什么时候客户端需要显式 bind

// 特殊情况:客户端需要固定端口
struct sockaddr_in client_addr;
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(12345);  // 指定客户端端口
client_addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
// 然后再调用 sendto()

需要显式绑定的场景:

  • 防火墙规则要求特定源端口

  • 服务器需要回连到客户端的特定端口

  • 特定的网络协议要求

总结

你的理解是正确的:网络设置确实需要"设置到内核区域"。这个客户端代码之所以能工作,是因为:

  1. 自动绑定机制sendto()第一次调用时会触发操作系统自动绑定

  2. 内核管理:所有socket操作最终都通过内核系统调用完成

  3. 设计哲学:客户端让操作系统自动管理细节,简化编程

这种设计体现了"约定优于配置"的原则,让常见的用例变得简单!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐