【Linux】网络地址 / socket 套接字

一. 网络地址

1. IP 地址

IP 地址 用于唯一标识和定位网络中不同的主机;

IP 地址分为两种:

  • IPv4 (Internet Protocol version 4): 由网络号和主机号组成, 使用 32 位二进制数表示, 也就是 4 字节的无符号整数, 通常使用点分十进制表示, 例如 192.168.0.1;
    网络号: 表示该设备所属的网络;
    主机号: 表示该设备在该网络中的编号;

  • IPv6 (Internet Protocol version 6): 使用 128 位二进制数表示, 16 字节的大小, 通常使用十六进制数字和冒号表示, 例如: ABCD:EF01:2345:6789:ABCD:EF01:2345:6789;

2. MAC 地址

MAC(Media Access Control), 也称为物理地址或硬件地址, 用于在局域网中唯一标识网络适配器(网卡);

MAC 地址 使用 48 位二进制数表示, 6 字节的大小, 通常使用十六进制数字和冒号表示, 例如: AB:CD:EF:01:23:45;

MAC 地址是数据链路层的一部分, 用于在局域网中寻找目标设备, 将数据包从源设备传输至目标设备;

3. 端口号

端口号(Port) 用于标识同一主机中的不同网络进程;

端口号 使用 16 位二进制数表示, 2 字节的大小, 取值范围为 [0, 65535];

端口号根据传输协议分为 TCP端口和 UDP端口, 不同的传输协议可以使用相同的端口号;

IP 地址 + 端口号 可以标识公网环境下, 唯一的网络进程;
一个进程可以绑定多个端口号, 但一个端口号不允许被多个进程绑定;

4. 传输层协议

TCP(Transmission Control Protocol) 传输控制协议 特点:

  • 传输层协议;
  • 有连接;
  • 可靠传输;
  • 面向字节流;

UDP(User Datagram Protocol) 用户数据报协议 特点:

  • 传输层协议;
  • 无连接;
  • 不可靠传输;
  • 面向数据报;

5. 网络字节序

由于不同主机的大小端可能不同, 所以TCP/IP 协议规定: 网络中传输的数据, 统一采用大端存储方案, 也就是网络字节序;

并且提供了主机字节序和网络字节序互相转换的相关函数;

#include <arpa/inet.h>

// 主机字节序转网络字节序
uint32_t htonl(uint32_t hostlong); // l 表示32位长整数
uint32_t htons(uint32_t hostshort); // s 表示16位短整数

// 网络字节序转主机字节序
uint32_t ntohl(uint32_t netlong); // l 表示32位长整数
uint32_t ntohs(uint32_t netshort); // s 表示16位短整数

二. socket 套接字

1. socket 常见API

#include <sys/types.h>
#include <sys/socket.h>

// 创建 socket 文件描述符(TCP/UDP	服务器/客户端)
int socket(int domain, int type, int protocol);

// 绑定端口号(TCP/UDP	服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);

// 开始监听 socket (TCP	服务器)
int listen(int socket, int backlog);

// 接收连接请求 (TCP	服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP	客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

2. sockaddr 结构

struct sockaddr 结构体是用来表示 IP 地址的标准结构体;

而 socket API是一层抽象的网络编程接口, 适用于各种底层网络协议, 所以 struct sockaddr 结构体就需要适用于各种协议的 IP 地址 的格式;

struct sockaddr 结构体衍生出了两个不同的结构体:

  • sockaddr_in 网络套接字, 适用于网络通信;
  • sockaddr_un 域间套接字, 适用于域间通信;

通信时, 根据16 位地址类型, 判断通信类型;
在这里插入图片描述

三. UDP 网络程序

使用 socket 套接字接口及 UDP 协议的实现简单网络通信, 客户端向服务器发送消息, 服务器接受消息后再转发至所有的客户端, 客户端接受消息并打印, 类似聊天室;

服务器端

框架

Server.hpp

#pragma once
#include <sys/types.h> 
#include <sys/socket.h>

class UdpServer
{
public:
    UdpServer()
    {}

    void Start()
    {}

    ~UdpServer()
    {}


private:
    // 网络
    int _sockfd;
    uint16_t _port;
    sockaddr_in _sockaddr;
};
1. 创建套接字

进行网络通信, 首先需要创建套接字, 使用 socket() 函数

#include <sys/types.h>
#include <sys/socket.h>

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

参数:

  • domain: 表示套接字的地址族, 常用的有 AF_UNIX(unix), AF_INET(IPv4), AF_INET6(IPv6);
  • type: 表示数据的传输方式, SOCK_STREAM(流格式传输) 和 SOCK_DGRAM(数据报传输);
  • protocol: 表示传输协议, 常用的有 IPPROTO_TCP 和 IPPTOTO_UDP; 若地址类型和数据传输方式只被一种协议支持, 那么 protocol 可以为 0, 表示自动推导传输协议;

返回值:

  • 若成功, 返回套接字(文件描述符); 若失败, 返回 -1;

Server.hpp

这里使用的 UDP 协议, 所以地址族选择 AF_INET, 传输方式选择 SOCK_DGRAM;

#pragma once
#include "Log.hpp" 
#include <sys/types.h> 
#include <sys/socket.h>

class UdpServer
{
public:
    UdpServer()
    {
    	// 创建 socket 文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)	// 若创建失败
        {
            LOG(FATAl, "socket fail");
            exit(1);
        } 
    }

private:
    // 网络
    int _sockfd;
};
2. 绑定 IP 地址和端口号

使用 bind() 函数进行绑定

#include <sys/types.h>
#include <sys/socket.h>

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

参数:

  • sockfd: socket 文件描述符;
  • addr: sockaddr 结构体变量的指针, 包含 IP 地址和端口号等内容;
  • addrlen: sockaddr 结构体变量的大小;

返回值:

  • 若成功, 返回 0; 若失败, 返回 -1;

这里是网络通信, 所以使用 sockaddr_in 结构体, 需包含两个头文件;

#include <netinet/in.h>
#include <arpa/inet.h>

sockaddr_in 结构体

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
  __SOCKADDR_COMMON (sin_);
  in_port_t sin_port;			/* Port number.  */
  struct in_addr sin_addr;		/* Internet address.  */

  /* Pad to size of `struct sockaddr'.  */
  unsigned char sin_zero[sizeof (struct sockaddr)
	   - __SOCKADDR_COMMON_SIZE
	   - sizeof (in_port_t)
	   - sizeof (struct in_addr)];
};

在这里插入图片描述

__SOCKADDR_COMMON 是一个宏函数, 使用 C 语言中一个语法 ## (拼接两个字符串);

/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

16 位地址类型的变量名实际上是 _SOCKADDR_COMMON 传入 sin 参数后, 拼接而成的;

sa_family_t sin_family;

端口号 in_port_t 类型 实际上是一个 2 字节, 16 位的无符号整数, 符合端口号的取值范围 [0, 65535];

typedef unsigned short int __uint16_t;
typedef __uint16_t uint16_t;

typedef uint16_t in_port_t;

IP 地址 in_addr 结构体, 其中包含了一个 32 位无符号整数, 存储 IP 地址;

typedef unsigned int __uint32_t;
typedef __uint32_t uint32_t;

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
  in_addr_t s_addr;
};

填充字段不使用, 一般用 0 填充;

可以使用 bzero 函数, 将变量置 0;

#include <strings.h>

void bzero(void *s, size_t n);

Server.hpp

端口号自行设置;

#pragma once
#include "Log.hpp" 
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>

class UdpServer
{
public:

    UdpServer(uint16_t port = 8888)
        :_port(port)
    {
        // 创建 socket 文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)	// 若创建失败
        {
            LOG(FATAl, "socket fail");
            exit(1);
        }   

        // 设置 ip地址 和 端口号
        bzero(&_sockaddr, sizeof(_sockaddr));

        _sockaddr.sin_family = AF_INET;	// 设置 16 位地址类型
        _sockaddr.sin_addr.s_addr = INADDR_ANY;	// INADDR_ANY 即 0, 表示本机的所有 IP
        _sockaddr.sin_port = htons(_port);	// 设置端口号, 转网络字节序


        // 绑定 socket
        int flag = bind(_sockfd, (sockaddr*)&_sockaddr, sizeof(_sockaddr));
        if (flag < 0)
        {
            LOG(FATAl, "bind fail");
            exit(1);
        }
    }

private:
    // 网络
    int _sockfd;	// socket 文件描述符
    uint16_t _port;	// 端口号
    sockaddr_in _sockaddr;	// sockaddr 结构
};

INADDR_ANY 即 0.0.0.0, 表示本机的所有 IP;
若主机有多个网卡, IP地址, 绑定某个具体的 IP 地址, 就无法接受其他 IP 地址的数据;

点分十进制的字符串(“0.0.0.0”), 转换为无符号短整数, 可以使用 inet_addr() 函数, 并且此函数在进行转换的同时, 还会将主机序列转换为网络序列;

3. 接受/发送消息

使用 recvfrom() 函数可以获取数据;

#include <sys/types.h>
#include <sys/socket.h>

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

参数:

  • sockfd: socket 文件描述符;
  • buf: 输出型参数, 缓冲区;
  • len: 缓冲区大小;
  • flags: 读取方式(阻塞/非阻塞);
  • src_addr: 输出型参数, 发送数据的客户端地址信息的结构体;
  • addrlen: 输入输出型参数, src_addr 结构体大小;

返回值:

  • 若成功, 返回实际读取的字节数; 若失败, 返回 -1;
void* Recv()
{
    while (1)
    {
    	// 创建缓冲区, 对端结构体;
        char buf[128];
        sockaddr_in src_addr;
        socklen_t len = sizeof(src_addr);
	
        bzero(&src_addr, sizeof(src_addr));	// 置零

        int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
        if (flag < 0)	// 若失败
        {
            LOG(FATAl, "recvfrom fail");
            continue;
        }
		
		/*
	        数据处理...
        */
    }
    return 0;
}

使用 sendto() 函数可以发送数据;

#include <sys/types.h>
#include <sys/socket.h>

// 读取信息(TCP/UDP	服务器/客户端)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd: socket 文件描述符;
  • buf: 缓冲区, 发送的数据;
  • len: 缓冲区大小;
  • flags: 读取方式(阻塞/非阻塞);
  • dest_addr: 接收数据的主机地址信息的结构体;
  • addrlen: dest_addr结构体大小;

返回值:

  • 若成功, 返回实际发送的字节数; 若失败, 返回 -1;
void* Send()
{
    string buf;
    sockaddr_in* user;
    while (1)
    {
 		/*
 		   获取消息, 接收端的 sockaddr 结构体...
 		*/
 		
 		
 		// 发送消息
        int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)user, sizeof(*user));
        if (flag < 0)	// 若失败
            LOG(FATAl, "sendto fail");
    }
    return 0;
}
4. 加入多线程

服务器端使用两个线程, 分别接收消息和发送消息; 一个堵塞队列, 保证线程的同步和互斥;
在接受消息后, 需保存发送端的 sockaddr 结构体, 并且在堵塞队列中推入 消息;
发送消息时, 从堵塞队列中推出 消息, 发送信息至所有已保存地址的 主机;

Server.hpp

#pragma once
#include "Log.hpp" 
#include "RingQueue.hpp" 
#include <stdlib.h> 
#include <strings.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <algorithm>



class UdpServer
{
public:
    UdpServer(uint16_t port = 8888)    // 端口号
        :_port(port), _recv(bind(&UdpServer::Recv, this)), _send(bind(&UdpServer::Send, this))
    {
        // 创建 socket 文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)	// 若创建失败
        {
            LOG(FATAl, "socket fail");
            exit(1);
        }   

        // 设置 ip地址 和 端口号
        bzero(&_sockaddr, sizeof(_sockaddr));

        _sockaddr.sin_family = AF_INET;	// 设置 16 位地址类型
        _sockaddr.sin_addr.s_addr = INADDR_ANY;	// INADDR_ANY 即 0, 表示本机的所有 IP
        _sockaddr.sin_port = htons(_port);	// 设置端口号, 转网络字节序


        // 绑定 socket
        int flag = bind(_sockfd, (sockaddr*)&_sockaddr, sizeof(_sockaddr));
        if (flag < 0)	// 若失败
        {
            LOG(FATAl, "bind fail");
            exit(1);
        }
    }

    void Start()
    {
        _recv.start();
        _send.start();
    }

    void* Recv()
    {
    	char buf[128];
        while (1)
        {
        	// 对端结构体;
            sockaddr_in src_addr;
            socklen_t len = sizeof(src_addr);

            bzero(&src_addr, sizeof(src_addr));
			
			// 接受消息
            int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
            if (flag < 0)
            {
                LOG(FATAl, "recvfrom fail");
                continue;
            }

            buf[flag] = 0;
            // cout << buf << endl;
            // 存储主机地址
            _guard.Lock();

            string user = inet_ntoa(src_addr.sin_addr) + to_string(ntohs(src_addr.sin_port));
            if (!_map.count(user))
                _map[user] = src_addr;

            _guard.Unlock();
			
			// 推入 消息
            _queue.Push(user+": "+buf);
        }
        return 0;
    }

    void* Send()
    {
        string buf;
        while (1)
        {
        	// 获取消息, 接收端的 sockaddr 结构体...
            _queue.Pop(buf);
            // cout << buf << endl;

            vector<sockaddr_in*> users;
            _guard.Lock();
            for (auto& user:_map)
                users.emplace_back(&user.second);
            _guard.Unlock();

			// 发送消息
            for (auto& user:users)
            {
                int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)user, sizeof(*user));
                if (flag < 0)
                    LOG(FATAl, "sendto fail");
            }
        }
        return 0;
    }

    ~UdpServer()
    {
        _recv.join();
        _send.join();
        close(_sockfd);
        LOG(DEBUG, "close");
    }

private:
    // 线程
    unordered_map<string,sockaddr_in> _map;	// 存储主机地址
    RingQueue<string> _queue;	// 堵塞队列
    LockGuard _guard;
    Thread _recv;
    Thread _send;

    // 网络
    int _sockfd;	// socket 文件描述符
    uint16_t _port;	// 端口号
    sockaddr_in _sockaddr;	// sockaddr 结构
};

Server.cc

#include "Server.hpp"


int main()
{
    UdpServer server;
    server.Start();

    return 0;
}

客户端

客户端相较于服务器端更简洁;
客户端在发送消息之前需要得知服务器端的地址;
和服务器端不同, 客户端不需要手动绑定 IP 地址与端口号, 避免指定重复的端口号, 操作系统会在首次传输数据时自动 bind, 而服务器的端口不能随意改变;

Client.hpp

#pragma once
#include "Log.hpp" 
#include "Thread.hpp" 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>

class UdpClient
{
public:
    UdpClient(const string& ip, uint16_t port)    // ip 和 端口号
        :_ip(ip), _port(port), _recv(bind(&UdpClient::Recv, this)), _send(bind(&UdpClient::Send, this))
    {
        // 创建 socket 文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAl, "socket fail");
            exit(1);
        }   

        // 保存服务端的 ip地址和端口号
        bzero(&_server, sizeof(_server));

        _server.sin_family = AF_INET;
        _server.sin_addr.s_addr = inet_addr(_ip.c_str());
        _server.sin_port = htons(_port);

    }


    void Start()
    {
        _recv.start();
        _send.start();
    }

    void* Recv()
    {
        char buf[128];
        while (1)
        {
            sockaddr_in src_addr;
            socklen_t len = sizeof(src_addr);

            bzero(&src_addr, sizeof(src_addr));

            // 接收消息
            int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
            _guard.Lock();
            if (flag < 0)
            {
                LOG(FATAl, "recvfrom fail");
                _guard.Unlock();
                continue;
            }

            buf[flag] = 0;
            cout << buf << endl;
            _guard.Unlock();
        }
        return 0;
    }

    void* Send()
    {
        string buf;
        while (1)
        {   
            cin >> buf;
            
            // 发送消息
            int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)&_server, sizeof(_server));
            _guard.Lock();
            if (flag < 0)
                LOG(FATAl, "sendto fail");
            _guard.Unlock();
        }
        return 0;
    }

    ~UdpClient()
    {
        _recv.join();
        _send.join();
        close(_sockfd);
        LOG(DEBUG, "close");
    }


private:

    LockGuard _guard;
    Thread _recv;
    Thread _send;

    int _sockfd;
    string _ip;
    uint16_t _port;
    sockaddr_in _server;
};

Client.cc

#include "Client.hpp"


int main(int argc, char* argv[])
{
    // 可以在命令行指定
    // if (argc != 3)
    //     LOG(FATAl, "argv fail");
    // UdpClient client(argv[1], atoi(argv[2]));
    // client.Start();

    // 也可以直接设置
    string ip = "127.0.0.1";
    uint16_t port = 8888;
    UdpClient client(ip, port);
    client.Start();

    return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值