【网络socket编程----预备知识和UDP服务器模拟实现】

news2024/10/6 19:56:16

文章目录

  • 一、预备知识
    • 1.1 理解IP地址和端口号
    • 1.2 认识TCP协议和UDP协议
    • 1.3 网络字节序
    • 1.4 socket编程接口和sockaddr结构
  • 二、封装 UdpSocket


一、预备知识

1.1 理解IP地址和端口号

众所周知,每台主机都有一个IP地址。而主机和主机之间通信,也需要依赖IP地址。源IP地址指的就是发送数据包的那个电脑的IP地址, 目的IP地址就是想要发送到的那个电脑的IP地址

IP地址可以帮一个主机找到要通信的目的主机,但是单单有IP地址,不能实现真正的通信。因为,主机之间通信的本质,是两个主机上搭载的软件之间的通信

每台主机上都会有各种不同的软件,而IP地址只能帮我们确定一台主机,那么我们该如何确定主机上真正参与通信的软件呢?

答:端口号合一帮助我们更好地标识服务端和客户端进程的唯一性。

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

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

综上所述,网络通信的本质就是进程(不同主机上的进程)间通信!

下面对进程间通信和网络通信做一个对比:

首先,进程间通信的前提是,必须让不同的进程看到同一份资源。而对于网络通信来说,这个“资源”就是网络。
其次,进程间通信的本质就是在做I/O操作。网络通信也是这样,无非就是要把自己的数据发出去,或者是要接收他人发出来的数据。

为了保证主机上进程的唯一性,我们可以采用PID,为什么还要用端口号呢?

答:单单就技术来讲,利用PID来实现两台主机上进程之间的通信时可以的,但是仍然使用端口号的目的是为了实现“解耦”,避免未来PID改变时,会影响网络通信的情况。
另外,当客户端要与服务器进行通信时,必须保证客户端每次都能找到服务器,也就是说,服务器的唯一性(IP+port)不能随意改变,而PID本身有很多不确定因素,所以不能用它来进行网络通信。
最后,不是所有的进程都要提供网络通信服务或请求,但是每个进程都需要有PID。

底层的操作系统是如何根据端口号找到指定的进程的?

答案是,在操作系统中,维护了一张哈希表,这张哈希表中每一个元素的key值就是一个端口号,value值就是指定进程对应的PCB的地址。所以,就可以根据这张表,通过端口号找到目标PCB,也就可以进行数据交换了。

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

在网络通信过程中,除了发送数据之外,还要把自己的IP和port发给对方,因为对方要根据IP+port发回数据。也就是说,在发送数据的时候,一定会多发送一些除通信数据之外的数据,而这部分多出来的数据是以协议的形式呈现的。

1.2 认识TCP协议和UDP协议

TCP(Transmission Control Protocol):

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

UDP(User Datagram Protocol):

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

这篇文章中,不对这两个协议做过多的解释,后面会有新的文章针对二者做出详细说明。

注意,这里的“可靠”和“不可靠”是中性词,没有好坏之分。

实现可靠传输的代价往往更高,这体现在代码实现和维护上面;而不可靠传输则不需要考虑这些东西。在实际应用中,我们往往需要根据实际情况,选择更适合的协议,不能在说法上“厚此薄彼”。

1.3 网络字节序

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

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

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

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//将32位整数从主机序列转为网络序列
uint16_t htons(uint16_t hostshort);//将16位整数从主机序列转为网络序列
uint32_t ntohl(uint32_t netlong);//将32位整数从网络序列转为主机序列
uint16_t ntohs(uint16_t netshort);//将16位整数从网络序列转为主机序列

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

1.4 socket编程接口和sockaddr结构

上文中说,IP+port可以标识网络中唯一一个进程,而实际上IP+port就叫做套接字(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域间套接字。网络套接字主要支持网络中跨主机之间的通信,同时也支持本地通信;而unix域间套接字只支持本地通信(会了网络套接字自然就会了这个);而原始套接字可以绕过传输层,直接访问底层个各种数据(抓包等的实现就是利用原始套接字)。

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同

为了支持不同的套接字通信方式,就必须有不同的接口。但是,接口的设计者不想设计出各种不同的接口,所以设计出了一种sockaddr结构,可以通过不同的参数完成不同的功能。

例如:
在这里插入图片描述

很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

IPv4的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些像bind 、accept函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下


二、封装 UdpSocket

开始写代码之前,先来认识几个接口:
在这里插入图片描述
第一个参数domain代表域(网络通信还是本地通信);第二个参数代表套接字提供的能力类型(流式服务/数据报服务);第三个参数代表使用的协议(一般默认为0)。而返回值是一个文件描述符(失败则返回-1)。

绑定套接字需要的接口:
在这里插入图片描述
绑定成功返回0,否则返回-1。

来看一下第三个参数的内部结构:

/* 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,如下:

typedef unsigned short int sa_family_t;
#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

可以看到,这里用到了宏的双#,它的意思是字符串合并,也就是将接收到的参数和family合并为一个字符串。

而对于in_addr:

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

它就是IP地址,一个32位的整数。
而我们经常见到的IP地址一般都是点分十进制的样子,而这里的确实一个四字节的数字。原因在于前者的可读性较强,但是在网络通信中,最主要的目的不是为了可读性,而是为了写代码方便。所以采用整数的形式,在OS内部会有一个特殊的结构体,将其转换为字符串。

另外,为了将字符串形式的IP地址转为整数,还需要使用一个接口:
在这里插入图片描述
这里再补充一个概念:
服务器的本质就是一个死循环,因此它是一个常驻内存的进程。我们熟知的另一个死循环的软件就是操作系统。

封装后的udp(udpServer.hpp)如下:

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <cstdlib>
#include <cerrno>
#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";
    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.创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error:" << errno << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            // 2.绑定port,ip
            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()); // 将IP转成整数并做字节序转换
            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);
            }
        }
        void start()
        {
            for (;;)
            {
                sleep(1);
            }
        }
        ~udpServer()
        {
        }

    private:
        uint16_t _port;
        string _ip;
        int _sockfd; // 文件描述符
    };
}

udpServer.cc内容如下:

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

using namespace std;
using namespace Server;

static void Usage(string proc)
{
  cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

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

  usvr->initServer();
  usvr->start();
  return 0;
}

运行结果如下:
在这里插入图片描述

其中出现的警告信息,是我们还没有编写的客户端,暂时不用在意。可以看到,服务器端是可以正常运行的。

下面再来完善服务器内部的工作,先来看一个接口:
在这里插入图片描述
该接口用于接收数据。

补充后的start函数内容如下:

void start()
{
    char buffer[1024];
    for (;;)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列转主机序列 2.整数IP地址转换为点分十进制
            uint16_t clientport = ntohs(peer.sin_port);
            string message = buffer;
            cout << clientip << "[" << clientport << "]# " << message << endl;
        }
    }
}

下面再来完善客户端代码,先来看一个接口:
在这里插入图片描述
该接口用于发送数据。

udpClient.hpp内容如下:

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <cstdlib>
#include <cerrno>
#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 intiClient()
        {
            // 1.创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket successfully" << " : " << _sockfd << endl;
            // 2.client必须要bind,但是不需要显示地(程序员自己实现)bind,由操作系统自动形成端口进行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;
    };
}

udpClient.cc内容如下:

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

using namespace std;
using namespace Client;

static void Usage(string proc)
{
  cout << "\nUsage:\n\t" << proc << "server_ip server_port\n\n";
}

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->intiClient();
  ucli->run();
  return 0;
}

Makefile内容如下:

cc=g++

.PHONY:all
all:udpClient udpServer

udpClient:udpClient.cc
	$(cc) -o $@ $^ -std=c++11
udpServer:udpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpClient udpServer

运行结果如下:
在这里插入图片描述
可以看到,程序正常运行。并且,由于Client的端口号是由操作系统自动生成的,所以两次连接后得到的端口号不一样。


本篇完,青山不改,绿水长流!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/476157.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

对比学习论文阅读:CoCLR算法笔记

标题&#xff1a;Self-supervised Co-training for Video Representation Learning 会议&#xff1a;NIPS2020 论文地址&#xff1a;https://dl.acm.org/doi/abs/10.5555/3495724.3496201 官方代码&#xff1a;https://www.robots.ox.ac.uk/~vgg/research/CoCLR/ 作者单位&…

软考算法-排序篇-上

数据排序 一&#xff1a;故事背景二&#xff1a;直接插入排序2.1 概念2.2 画图表示2.3 代码实现2.4 总结提升 三&#xff1a;希尔排序3.1 概念3.2 画图表示3.3 代码实现3.4 总结提升 四&#xff1a;直接选择排序4.1 概念4.2 画图表示4.3 代码实现4.4 总结提升 五&#xff1a;堆…

组播PIM协议

PIM&#xff08;Protocol Independent Multicast&#xff09;称为协议无关组播&#xff08;组播分发树&#xff09;。这里的协议无关指的是与单播路由协议无关&#xff0c;即PIM不需要维护专门的单播路由信息。作为组播路由解决方案&#xff0c;它直接利用单播路由表的路由信息…

LeetCode:142. 环形链表 II

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340; 算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 题解目录 一、&#x1f331;[142. 环形链表 II](https://leetcode.cn/problems/linked-l…

瑞吉外卖:后台系统登录功能

文章目录 需求分析代码开发创建实体类导入返回结果类Rcontroller、service与mapperlogin.html 需求分析 点击登录按钮后&#xff0c;浏览器以POST方式向employee/login提交username和password&#xff0c;服务器经过处理后向浏览器返回某种格式的数据&#xff0c;其中包含&…

Java SE(十一)之异常处理(Exception)

文章目录 异常概述1.什么是异常&#xff1f;2.为什么要异常&#xff1f; 异常体系及分类1.运行时异常2.编译时异常 异常处理1.JVM默认处理方案2.try…catch…3.throw & throws&#xff08;1&#xff09;抛出异常throw&#xff08;2&#xff09;声明异常throws&#xff08;3…

Android Studio制作手机App:通过手机蓝牙(Bluetooth)与STM32上的低功耗蓝牙(HC-42)连接通信,实现手机端对单片机的控制。

背景&#xff1a; 本文的内容是针对单片机蓝牙模块&#xff08;HC-42&#xff09;开发的手机App。在这之前&#xff0c;我想先声明一点&#xff0c;手机与手机间的蓝牙连接方式”与“手机与HC间的蓝牙连接方式”是不一样的。原因就是手机搭配的是“经典蓝牙”模块&#xff0c;…

HTML【前端基础】

目录 1.HTML 结构 1.1 HTML 标签 1.2 HTML 文件基本结构 1.3 标签层次结构 1.4 快速生成代码框架 2.HTML 常见标签 2.1 注释标签 2.2 标题标签: h1-h6 2.3 段落标签&#xff1a;p 2.4 换行标签: br 2.5 格式化标签 2.6 图片标签: img 2.7 超链接标签: a 2.8 表格…

Android Java 音频采集 AudioRecord

在 Android Java 应用中&#xff0c;一般用 AudioRecord 管理从平台的音频输入设备采集音频数据所需的资源。音频采集和音频播放密切关系&#xff0c;Android 系统中 Java AudioRecord 和 AudioTrack 在许多方面&#xff0c;都有着很高的相似性&#xff0c;无论是代码的目录组织…

java基础知识——25.异常

这篇文章&#xff0c;我们来讲一下java的异常体系 目录 1.异常概述 2 java的异常继承体系 2.1 编译时异常 2.2 运行时异常 2.3 底层原理 2.4 异常的作用 3.异常的处理方式 3.1 JVM默认的处理方式 3.2 自己处理&#xff08;捕获异常&#xff09; 3.2.1自己处理的4个问…

端到端NVMe?| NVMe-OF或FC-NVMe

声明 主页&#xff1a;元存储的博客_CSDN博客 依公开知识及经验整理&#xff0c;如有误请留言。 个人辛苦整理&#xff0c;付费内容&#xff0c;禁止转载。 内容摘要 前言 NVMe全称是Nonvolatile Memory Express&#xff08;非易失性内存标准&#xff09;&#xff0c;在它首次…

( 字符串) 647. 回文子串 ——【Leetcode每日一题】

❓647. 回文子串 难度&#xff1a;中等 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串&#xff0c;即使…

JSP 的本质原理解析:“编写的时候是JSP,心里想解读的是 java 源码“

JSP 的本质原理解析&#xff1a;“编写的时候是JSP&#xff0c;心里想解读的是 java 源码” 文章目录 JSP 的本质原理解析&#xff1a;"编写的时候是JSP&#xff0c;心里想解读的是 java 源码"每博一文案1. JSP 概述2. 第一个 JSP 程序3. JSP 的本质就是 Servlet4. J…

appium的手动安装步骤教程及appium-doctor报错解决集合

前言 相信你不少软件测试行业小伙伴应该在用npm安装appium或者是cpm安装appium途中也碰到下面一些报错吧&#xff0c;接下来Darren洋教你改为手动安装appium吧&#xff01;整理不易&#xff0c;请点赞加关注后查看。 一、安装Node.js 下载地址&#xff1a; Previous Releases …

【五一创作】某头条参数破解并实现界面化搭建

某条参数破解并实现界面化搭建 前言效果展示难点参数逆向破解_signatureac_signatures_v_web_id 界面化实现总结 前言 趁着日常闲余时间&#xff0c;想着搞一搞某条的反爬&#xff0c;练练手&#xff0c;想到自己很久没开发过前端界面了&#xff0c;有点生疏&#xff0c;也趁此…

PCL学习二:PCL基础应用教程

参考引用 PCL Basic UsagePCL 点云库官网教程 1. pcl_viewer 基本使用 1.1 pcl_viewer 安装测试 pcl_data 源码克隆$ git clone https://github.com/PointCloudLibrary/data.git进入 /pcl_data/tutorials&#xff08;如下图&#xff09;$ cd ~/pcl_data/tutorials # 此处为重…

IDEA常用提升效率的操作小记

IDEA目前是使用最广泛的Java开发工具之一了&#xff0c;虽然是收费的&#xff0c;但是也提供了免费的社区版&#xff0c;并且收费版也支持使用github的开源项目&#xff0c;使用免费license&#xff0c;虽然每年都要续&#xff0c;我用的就是开源项目申请的免费license。 开发…

【Pytorch基础教程39】torch常用tensor处理函数

note 文章目录 note一、tensor的创建二、tensor的加减乘除三、torch.argmax()函数四、gathter函数小栗子1小栗子2&#xff1a;如果每行需要索引多个元素&#xff1a; 四、针对某一维度的操作五、改变维度、拼接、堆叠等操作Reference 一、tensor的创建 torch.tensor会复制data…

STM32配置ADC2(DMA)进行采集 DAC 输出-2

0.一定要先看上一节&#xff1a;STM32配置ADC2&#xff08;DMA&#xff09;进行采集 DAC 输出-2 1.实验目标 在上一节的基础上&#xff0c;我们把 DAC&#xff08;三角波&#xff09;给集成进来&#xff0c;实现按下按键输出三角波&#xff0c;通过串口发送数据给电脑&#x…