【Linux网络】网络编程套接字 -- 基于socket实现一个简单UDP网络程序

认识端口号

我们把数据从A主机发送到B主机,是目的吗?不是,真正通信的不是这两个机器!其实是这两台机器上面的软件(人)

数据有IP(公网)标识一台唯一的主机,用谁来标识各自主机上客户或者服务进程的唯一性呢?
为了更好的表示一台主机上服务进程的唯一性,我们采用端口号port标识服务器进程,客户端进程的唯一性!

端口号(port)是传输层协议的内容:

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

ip地址(主机全网唯一性) + 该主机上的端口号,标识该服务器上进程的唯一性
IP保证全网唯一,port保证在主机内部的唯一性

主机上对应的服务进程,在全网中是唯一的一个进程。
网络通信的本质:其实就是进程间通信

  1. 需要让不同的进程,先看到同一份资源—网络
  2. 通信就是在做IO,所以我们所有的上网行为,无外乎两种:我要把我的数据发出去、我要收到别人给我发的数据

进程已经有pid,为什么要有port呢?

  1. 系统是系统,网络是网络,单独设置—系统与网络解耦
  2. 需要客户端每次都能找到服务器进程—服务器的唯一性不能做任何改变— IP+port不能随意改变不会轻易改变。
  3. 不是所有的进程都要提供网络服务或者请求,但是所有的进程都需要pid。

进程+port–>网络服务进程
底层OS如何根据port找到指定的进程:OS内部采用hash方案,在OS内部维护了一个基于端口号的哈希表,key就是端口号,value就是task_struct的地址。有这个端口号就可以找到PCB,继而找到文件描述符表,文件描述符对象,对象找到了那么这个文件的缓冲区也就能找到,然后就可以将数据拷贝到缓冲区,最后就相当于我们将网络数据放到了文件中,如同读文件一样就将数据读上去了。

一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;

理解源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号目的端口号。 就是在描述 “数据是谁发的,要发给谁”;

认识TCP(Transmission Control Protocol 传输控制协议)协议:

  • 传输层协议
  • 有连接(相当于打电话必须先接通才能通话)
  • 可靠传输
  • 面向字节流
    认识UDP协议

认识UDP(User Datagram Protocol 用户数据报协议)协议:

  • 传输层协议
  • 无连接(相当于发送邮件只需要知道你的邮箱地址不需要你同意直接就能发给你)
  • 不可靠传输
  • 面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  3. 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  4. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  5. 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可;

在这里插入图片描述

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

在这里插入图片描述

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

处理字节序函数 htonl、htons、ntohl、ntohs

在这里插入图片描述
函数原型:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

函数作用:
将数据在不同字节序之间进行转换。

函数的详细介绍:

  • htonl() 函数:将一个 32 位无符号整数(unsigned int)从本地字节序转换为网络字节序(大端字节序)。
  • htons() 函数:将一个 16 位无符号整数(unsigned short)从本地字节序转换为网络字节序(大端字节序)。
  • ntohl() 函数:将一个 32 位无符号整数(unsigned int)从网络字节序(大端字节序)转换为本地字节序。
  • ntohs() 函数:将一个 16 位无符号整数(unsigned short)从网络字节序(大端字节序)转换为本地字节序。

socket

socket编程接口

// 创建 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);
  • 网络套接字编程(多应用于:网络跨主机之间多主机通信、支持本地通信)
  • 原始套接字(可以跨过传输层向下访问更底层的接口)
  • unix或间套接字(仅本地通信)

按道理要实现上述三种套接字应该要三套不同的接口,但是设计者只设计了一套接口,通过不同的参数解决所有网络或其他场景下的通信问题

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同
在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构:

struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

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

虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型、端口号、IP地址。

in_addr结构:

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

in_addr用来表示一个IPv4IP地址。其实就是一个32位的整数。

显示当前户籍UDP连接状况与端口号的使用情况:

sudo netstat -nuap

结尾实现UDP程序的socket接口使用解析

socket

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

函数作用:
用于创建一个新的网络套接字的系统调用。

函数参数:
domain 参数指定了网络协议族:

  • AF_INET 表示 IPv4 协议
  • AF_INET6 表示 IPv6 协议
  • AF_UNIX 表示 Unix 域协议

type 参数指定了套接字的类型

  • SOCK_STREAM 表示面向连接的流套接字
  • SOCK_DGRAM 表示无连接的数据报套接字
  • SOCK_RAW 表示原始套接字。

protocol 参数指定了使用的协议

  • IPPROTO_TCP 表示 TCP 协议
  • IPPROTO_UDP 表示 UDP 协议
  • 参数设置为 0 时,系统会根据指定的 domain 和 type 参数选择一个默认的协议。这通常是最常用的协议,例如对于 AF_INET 和 SOCK_STREAM 的组合,通常使用的协议是 TCP(即 IPPROTO_TCP)。

使用 socket() 函数的一般流程如下:

  1. 创建一个套接字:调用 socket() 函数,指定 domain、type 和 protocol 参数,返回一个新的套接字描述符。
  2. 绑定套接字到本地地址:调用 bind() 函数,将套接字和一个本地地址绑定,以便其他程序可以通过该地址找到该套接字。
  3. 监听连接请求:如果创建的是面向连接的流套接字,可以调用 listen() 函数,开始监听连接请求。
  4. 接受连接请求:如果创建的是面向连接的流套接字,可以调用 accept() 函数,接受一个连接请求,返回一个新的套接字描述符,用于与客户端通信。
  5. 发送和接收数据:调用 send() 和 recv() 函数,向对方发送数据或接收对方发送的数据。
  6. 关闭套接字:调用 close() 函数,关闭套接字描述符,释放相关资源。

处理 IP 地址的函数

在这里插入图片描述

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
  • inet_aton() 函数:将一个字符串形式的 IP 地址转换为一个二进制形式的 IP 地址。如果转换成功,函数返回非零值,否则返回零。
  • inet_addr() 函数:将一个字符串形式的 IP 地址转换为一个 32 位的整数,该整数表示为网络字节序。如果转换成功,函数返回一个非零值(即返回一个网络字节序表示的 IP 地址),否则返回 INADDR_NONE。
  • inet_network() 函数:将一个字符串形式的 IP 地址的网络部分转换为一个 32 位的整数,该整数表示为网络字节序。
  • inet_ntoa() 函数:将一个二进制形式的 IP 地址转换为一个字符串形式的 IP 地址。注意,该函数返回的是一个指向静态缓冲区的指针,因此不要将其作为返回值传递给其他函数。
  • inet_makeaddr() 函数:根据网络号和主机号创建一个 IP 地址。
  • inet_lnaof() 函数:从一个二进制形式的 IP 地址中提取主机号部分。
  • inet_netof() 函数:从一个二进制形式的 IP 地址中提取网络号部分。

初始化sockaddr_in

sockaddr_in是一个 IPv4 地址结构体,用于存储 IP 地址和端口号信息。在使用套接字函数时,通常需要将地址信息存储在 sockaddr_in 结构体中,并将其作为参数传递给函数

	struct sockaddr_in local; // 定义了一个变量,栈,用户
    bzero(&local, sizeof(local));//用于将指定的内存区域清零
    local.sin_family = AF_INET;//将结构体成员 sin_family 设置为 AF_INET,表示使用 IPv4 地址族
    local.sin_port = htons(_port); 
    //结构体成员 sin_port 设置为要使用的端口号,使用 htons() 函数将端口号转换为网络字节序(大端字节序)
    
    local.sin_addr.s_addr = inet_addr(_ip.c_str());
    //将结构体成员 sin_addr 设置为要使用的 IP 地址,使用 inet_addr() 函数将 IP 地址转换为网络字节序(大端字节序)
    //在实际开发中,可以使用 inet_pton() 函数将字符串形式的 IP 地址转换为一个 struct in_addr 类型的结构体,该结构体包含了 IP 地址的二进制表示

bind

int bind(int socket, const struct sockaddr *address,
              socklen_t address_len);

函数作用:
用于将一个本地地址(IP 地址和端口号)与一个套接字关联起来的函数

函数参数:

  • socket 参数是一个指定了套接字的文件描述符。
  • addr 参数是一个指向 struct sockaddr 类型的结构体的指针,该结构体包含了要绑定的本地地址信息。
  • addrlen 参数是 addr 结构体的长度

函数返回值:
返回值为 0 表示绑定成功,-1 表示绑定失败,错误码保存在 errno 变量中。

recvfrom

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
              int flags, struct sockaddr *restrict address,
              socklen_t *restrict address_len);

函数作用:
用于从已连接或未连接的套接字接收数据的函数

函数参数:

  • socket 参数是指定了要接收数据的套接字的文件描述符。
  • buf 参数是一个指向接收数据的缓冲区的指针。
  • len 参数是缓冲区的大小。
  • flags 参数是一组标志位,可以用来指定接收数据的行为。为0表示默认
  • address 参数是一个指向 struct sockaddr 类型的结构体的指针,用于存储发送数据的远程地址。
  • addrlen 参数是 src_addr 结构体的长度。

函数返回值:
函数返回值为接收到的数据的字节数,如果没有数据可用,则返回 0。如果发生错误,则返回 -1,错误码保存在 errno 变量中

sendto

ssize_t sendto(int socket, const void *message, size_t length,
              int flags, const struct sockaddr *dest_addr,
              socklen_t dest_len);

函数作用:
用于向已连接或未连接的套接字发送数据的函数

函数参数:

  • socket 参数是指定了要发送数据的套接字的文件描述符。
  • buf 参数是一个指向要发送数据的缓冲区的指针。
  • len 参数是要发送数据的字节数。
  • flags 参数是一组标志位,可以用来指定发送数据的行为。
  • dest_addr 参数是一个指向 struct sockaddr 类型的结构体的指针,用于指定接收数据的远程地址。
  • dest_len 参数是 dest_addr 结构体的长度。

函数返回值:
函数返回值为发送数据的字节数,如果发生错误,则返回 -1,错误码保存在 errno 变量中。


实现一个简单的UDP网络程序

封装服务器相关代码

udpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

namespace Server
{
    using namespace std;

    static const string defaultIp = "0.0.0.0"; //TODO
    static const int gnum = 1024;

    enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};

    class udpServer
    {
    public:
        udpServer(const uint16_t &port, const string &ip = defaultIp)
        :_port(port), _ip(ip), _sockfd(-1)
        {}
        void initServer()
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            // 2. 绑定port,ip(TODO)
            // 未来服务器要明确的port,不能随意改变
            struct sockaddr_in local; // 定义了一个变量,栈,用户
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = inet_addr(_ip.c_str());
            //local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法
            int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
            if(n == -1)
            {
                cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            // UDP Server 的预备工作完成
        }
        void start()
        {
            // 服务器的本质其实就是一个死循环
            char buffer[gnum];
            for(;;)
            {
                // 读取数据
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); //必填
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
                // 1. 数据是什么 2. 谁发的?
                if(s > 0)
                {
                    buffer[s] = 0;
                    string clientip = inet_ntoa(peer.sin_addr); //1. 网络序列 2. int->点分十进制IP
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << clientip <<"[" << clientport << "]# " << message << endl;
                }
            }
        }
        ~udpServer()
        {
        }
    private:
        uint16_t _port;
        string _ip; // 实际上,一款网络服务器,不建议指明一个IP
        int _sockfd;
        // func_t _callback; //回调
    };
}

udpServer.cc

#include "udpServer.hpp"
#include <memory>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "nUsage:nt" << proc << " local_portnn";
}

// ./udpServer port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<udpServer> usvr(new udpServer(port));
    usvr->initServer();
    usvr->start();

    return 0;
}

封装客户端相关代码

udpClient.hpp

#pragma once

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

namespace Client
{
    using namespace std;

    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport) 
        : _serverip(serverip),_serverport(serverport), _sockfd(-1), _quit(false)
        {}
        void initClient()
        {
            // 创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            // 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要
            // 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind
        }
        void run()
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);

            string message;
            while(!_quit)
            {
                cout << "Please Enter# ";
                cin >> message;

                sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        bool _quit;
    };
} // namespace Client

udpClient.cc


#include "udpClient.hpp"
#include <memory>

using namespace Client;

static void Usage(string proc)
{
    cout << "nUsage:nt" << proc << " server_ip server_portnn";
}

// ./udpClient server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));

    ucli->initClient();
    ucli->run();

    return 0;
}

实验结果

在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出??