LinuxSocket套接字编程

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<<"进入判断"<

4.UdpServer.hpp文件总代码

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

using namespace LogModule;

using func_t =std::function;

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<<"进入判断"<

5.实例实现

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

#include 
#include 
#include "UdpServer.hpp"
#include  
std::string defaulthandler(const std::string& message)
{
    std::cout<<"打印"< usvr=std::make_unique(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 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cerr<<"Usage:"<0)
        {
            buffer[m]=0;
            std::cout<

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

你可能感兴趣的:(Linux,服务器,网络,运维)