0


linux篇【12】:计算机网络<前序>—网络基础+udp套接字

一.网络基础

1.认识** "协议" **

"协议" 是一种约定

计算机之间的传输媒介是光信号和电信号. 通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息. 要想传递各种不同的信息, 就需要约定好双方的数据格式.

    计算机生产厂商有很多; 

    计算机操作系统, 也有很多; 

    计算机网络硬件设备, 还是有很多; 

    如何让这些不同厂商之间生产的计算机能够相互顺畅的通信? 就需要有人站出来, 约定一个共同的标准, 大家都来遵守, 这就是 **网络协议**;

计算机技术通信协议是计算机网络产生于发展的两个最基本的因素

协议——计算机的视角,如何看待协议:① 体现在代码逻辑上 ② 体现在数据上

以寄快递为例:你和卖家沟通好,买一个鼠标,实际上快递员给你的是
一个包裹,里面有鼠标,

实际上多给了我一些东西,多了一张
快递单
快递单是一块数据=>快递公司和快递点,快递小哥之间的协议。
为了维护协议,一定要在被传输的数据上,新增其他数据(协议数据)

举例:

HTTP协议是超文本传输协议;DNS协议为域名解析协议;FTP协议为文件传输协议;SMTP协议为电子邮件传输协议

**2.协议分层 **

(1)软件分层

软件是可以分层的,为什么要分层?
1.软件在分层的同时,也把问题归类的
2.分层的本质:软件上解耦
3.便于工程师进行软件维护
网络本身的代码,就是层状结构!

(2)协议分层

层状结构下的网络协议,我们认为,同层协议 都可以认为自已在和对方直接通信,忽略底层细节同层之间一定都要有自己的协议。
在下面这个例子中, 我们的协议只有两层(汉语协议和电话机协议); 但是实际的网络通信会更加复杂, 需要分更多的层次。分层最大的好处在于 "封装",面向对象例子。

3.OSI****七层模型

OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型, 是一个逻辑上的定义和规范;

    把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机; 

    OSI 七层模型是一种框架性的设计方法,其最主要的功能就是帮助不同类型的主机实现数据传输; 

    它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统,不同的网络之间实现可靠的通讯; 

    但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来学习

在OSI模型网络分层当中,自下而上,下层为上层提供服务,下层将从上层接的信息增加一个头部

4.TCP/IP**五层(或四层)模型**

TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。

TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求.

物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.

数据链路层: 负责设备之间的数据帧的传输
和识别,完成帧同步,
差错控制,流量管理,链路管理。
例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准。交换机(Switch)工作在数据链路层.

网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.

传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.

应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层

**物理层我们考虑的比较少. 因此很多时候也可以称为 TCP/IP四层模型. **

一般而言

对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容;

对于一台路由器, 它实现了从网络层到物理层;

对于一台交换机, 它实现了从数据链路层到物理层;

对于集线器, 它只实现了物理层。

5.网络和操作系统之间的关系

(1)体系结构直接决定, 数据包在主机内进行流动的时候,一定是要进行自顶向下(封包)或者自底向上(解包)进行流动的。以前的所有的IO都是这样的。

(2)tcp/ip协议和操作系统之间的关系是:操作系统内部,有一个模块,就叫做tcp/ip协议(传输层和网络层),网络协议栈是隶属于OS的。
(3)同层协议都认为自已在和对方直接通信——所以每一层都要有自己的协议
(4)重谈协议——计算机的视角,如何看待协议:① 体现在代码逻辑上 ② 体现在数据上
以寄快递为例:你和卖家沟通好,买一个鼠标,实际上快递员给你的是一个包裹,里面有鼠标,
实际上多给了我一些东西,多了一张快递单快递单是一块数据=>快递公司和快递点,快递小哥之间的协议。为了维护协议,一定要在被传输的数据上,新增其他数据(协议数据)

6.数据包的封装(封包)和解包,分用

    不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame). 

    应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装 

(Encapsulation).

    首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息. 

    数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理

网络传输数据的本质就是数据不断的封装和解包

(1)下图为数据封装,解包的过程

我们把每一层要交付给下一层的数据,给他添加上本层的”多出来的协议数据”(报头)拼接在原始数据的开头

(2)分用

有效载荷的分用过程:数据包添加报头的时候,也要考虑未来解包的时候,将自己的有效载荷交付给上层的哪一个协议!

下图为数据分用的过程:

两个结论:(大部分协议的公共属性)
1.一般而言,任何报头属性里面,一定要存在的一些字段支持,我们进行封装和解包,即:报头中一定要存着用于 区分报头和有效载荷 的数据
2.一般而言,任何报头属性里面,一定要存在的一些字段支持我们进行分用。即:报头中一定要存着用于 得知报文的有效载荷要给上层哪个协议 的数据

7.局域网(以太网)通信的原理

(1)局城网中两台主机可以互相通信

如果两台主机,处于同一个局城网。这两台主机可以直接通信——以太网,一种局域网的标准(以太——物理学界太空中不存在的物质叫以太,为了致敬命名以太网)以太网:站在系统的角度 就是 两台主机之间的临界资源。

(2)局域网通信原理

1.每一台主机都要有唯一的标识:该主机对应的MAC地址!
2.任何一台主机,在任何时刻,都可以随时发消息——碰撞域——无法准确的听到对应的消息——识别发生了碰撞(碰撞检测)——碰撞避免——等不碰撞了过一会儿再发消息

(3)MAC****地址和IP地址

举例:从北京往云南去旅游:常识告诉我们,一般我们在进行路线选择的时候,我们一般有两套地址:
1.从哪里来<源IP>,到哪里去<目的IP>——IP地址:源IP,目的IP
2.上一站从哪里来<源mac地址>,下一站要去哪里<目标mac地址>(由“到哪里去<目的IP>”决定)——MAC地址: 源mac地址,目标mac地址
MAC地址:用来在局域网中,标定主机的唯一性。
IP地址:用来在广域网(公网),标定主机的唯一性。

IP****地址和MAC地址详细解释:

IP协议有两个版本, IPv4和IPv6. 我们整个的课程, 凡是提到IP协议, 没有特殊说明的, 默认都是指IPv4

IP地址是在IP协议中, 用来标识网络中不同主机的地址;

对于IPv4来说, *IP***地址是一个4字节, 32位的整数; **

我们通常也使用** "点分十进制" 的字符串表示IP地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;**

MAC地址用来识别数据链路层中相连的节点;

长度为48位, 及6个字节**.****一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19) **

在网卡出厂时就确定了, 不能修改. mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址, 可能会冲突; 也有些网卡支持用户配置mac地址).

IP协议的两个版本, IPv4和IPv6:

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结构体指针做为参数

示例:

路由器可看做一个主机同时横跨了两个局域网

所有的IP向上的协议,发送和接受主机看到的数据是一模一样的
网络 -> IP网络,IP协议屏蔽了底层网络的差异! ! !

数据“你好”从客户发出,不断封装,到以太网驱动程序完成最后封装,再通过以太网传输给路由器下的以太网驱动程序,路由器下的以太网驱动程序解包数据,传给路由器,路由器发现这个数据是要传给IPB的,再通过路由器下的以太网驱动程序封装,通过令牌环传输给目标主机所在网络,自底向上解包传输

(4)数据包传输通过路由器转发

网络传输数据的本质就是数据不断的封装和解包。****路由器可看做一个主机同时横跨了两个局域网

所有的数据,必须在”网线”上跑!

二.网络编程套接字

1.源IP地址和目的IP地址

IP地址:用来在广域网(公网),标定主机的唯一性。

源IP地址:通信主机的源主机

目的IP地址:通信主机目的主机

两主机可以在同一个局域网也可以不在。

2.端口号,套接字组成介绍

我们在网络通信的时候,不止是让两台主机通信。实际上,在进行通信的时候,不仅仅要考虑两台主机间互相交互数据。本质上讲,进行数据交互的时候是用户和用户在进行交互。用户的身份,通常是用程序体现的。程序一定是在运行中——进程!
主机间在通信的本质是:在各自的主机上的两个进程在互相交互数据!
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方

IP :确保主机的唯一性
端口号(port):确保该主机上某一个进程的唯一性(则一个进程只能占用一个端口号)
IP:PORT = 标识互联网中唯一的一个进程!——>这两个合起来叫 socket(套接字)(翻译是插座)
网络通信的本质:就是进程间通信! ! !

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

端口号是一个2字节16位的整数;类型是uint16_t,不过传uint32_t也可以,最终会截断成uint16_t。

端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;

一个端口号只能被一个进程占用(一个进程可以有多个端口号,但一个端口号不可以对应多个进程,只要保证从端口号到进程的数据链路是唯一的

3.理解** "端口号" "进程ID"(端口号的意义)**

pid 在进程管理中表示唯一一个进程,端口号在网络通信中表示唯一一个进程的。

区别

端口号是网络通信的概念,pid是进程管理的概念。如果我们非要用进程pid在网络通信中标识唯一一个进程,又在进程管理中标识唯一一个进程,即让进程pid两用,本质上是可以的。但是这增加了网络通信和进程管理的强耦合性。端口号更侧重于表示这个进程是需要进行网络通信的,没有端口号就表示这个进程只在本地运行而不进行网络通信。(类似于学号和身份证号,要用学号表示一个学生在学校的唯一性)

总结:
端口号的意义:①使网络模块和进程管理模块解耦。②
有端口号就表示这个进程是网络进程,没有端口号可以不考虑该进程的网络功能。

4.源端口号和目的端口号

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

源IP:源端口, 目的IP:目的端口——两个socket对

5.TCP协议与UDP****协议

(1)TCP****协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。

  • 传输层协议
  • 有连接(要有建立连接的预备工作)
  • 可靠传输(可靠性:丢包重传,数据乱序排序等,但是会做更多工作,比较复杂。使用实例:例如转账不能丢包必须用TCP协议
  • 面向字节流

(2)UDP****协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论。

  • 传输层协议
  • 无连接(不需要建立连接,直接发数据)
  • 不可靠传输(虽然无可靠性,但是做的工作很少,是简单协议。使用实例:例如全球直播就用UDP协议即可,网好就看,网不好就别看)
  • 面向数据报

6.网络字节序

(1)规定:网络字节序默认是大端

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

①发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

②接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

③因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

④*TCP/IP***协议规定,网络数据流应采用大端字节序,**数据的低权职位保存在内存的高地址中

⑤不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

⑥如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

(2)网络和主机字节序的转换函数

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

uint32_t htonl (uint32_ t hostlong); ——htonl(host to net 主机转网络)

①这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示以32位的长整数为单位从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

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

③如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

三.socket套接字编程接口

socket头文件:

man socket,man htons,man inet_ addr查看所有头文件

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

**1.socket **常见API(套接字编程接口)

    创建 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****结构(套接字的地址结构类型定义)

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

两个
地址结构类型
struct sockaddr_ in——网络套接字;struct sockaddr_ un——域间套接字;上面的接口都有一个统一类型的参数叫 struct sockaddr* address(下图第一个结构体),当我们传入struct sockaddr_ in 或 struct sockaddr_ un 类型的结构体指针时,会强转成 struct sockaddr 类型的指针。这个struct sockaddr 的结构体再去前16位地址查看,如果是AF_INET ,就当做网络套接字去用,提取出16位端口号和32位IP地址;如果是AF_UNIX,就是
域间套接字,提取出108字节路径名。

3.套接字接口

(1)创建一个套接字 socket

man 2 socket

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

domain:socket网络通信的域——网络通信 (AF_INET /PF_INET )(或 本地通信 (AF_UNIX))。现在只用AF_INET 网络通信(有的地方把AF_INET写成PF_INET也是正确的)
type:套接字类型——决定了我们通信的时候对应的报文类型(流式 / 用户数据报式)

    流式套接:SOCK_STREAM ——用于**TCP****协议**

    用户数据报式套接:SOCK_DGRAM ——用于**UDP****协议**

protocol:协议类型——网络应用中设置为 0。(因为AF_INET+SOCK_STREAM—默认是TCP套接字;****AF_INET+SOCK_DGRAM—默认是UDP套接字

返回值:成功返回文件描述符(套接字描述符),错误返回-1并设置错误码(套接字类型本质就是文件描述符

(2)绑定网络信息 bind

man 2 bind

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

sockfd:套接字这个文件描述符。addr:传入我们自己创建的信息 struct sockaddr_in local 的地址,然后把它强转成struct sockaddr类型结构体,内部会自动识别是什么类型的套接字做绑定。addrlen:sockaddr类型结构体 的大小

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

例如:if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)

(3)把字符串风格的IP地址转为4字节地址 inet_addr ,4字节转字符串 inet_ntoa

①inet_addr

in_addr_t inet_addr(const char cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)注意:这类函数在转变IP风格时都会*自动进行主机字节序和网络字节序之间的转换。

返回值:成功返回IP对应的网络字节序的数;失败返回INADDR_NONE;

in_addr_t就是4字节类型

②inet_ntoa

char *inet_ntoa(struct in_addr in); 把4字节IP地址转为字符串风格的IP地址并返回。

返回值:inet_ntoa这个函数返回了一个char*(错误返回nullptr), 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。inet_ntoa函数把这个返回结果放到了静态存储区static char buffer[],这个时候不需要我们手动进行释放。

注意:这类函数在转变IP风格时都会
自动进行主机字节序和网络字节序之间的转换。

例子: std::string peerIp = inet_ntoa(peer.sin_addr); //拿到了对方的IP

inet_ntoa不是线程安全的函数

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

    思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?——不一定

    在APUE中, 明确提出inet_ntoa不是线程安全的函数; 

    但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁; 

    同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题; 

    在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

(4)网络服务 recvfrom 与 sendto

①udp特有的 recvfrom读取套接字中的信息

 man recvfrom

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

从特定套接字 sockfd中读取数据到缓冲区buf中,buf大小为len,flags设为0——阻塞式读取

src_addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入src_addr中。(src_addr的类型是套接字类型指针struct sockaddr,传入的网络套接字类型struct sockaddr_in需要强转成此类型指针 struct sockaddr*。)

addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)

返回值:返回读到的字节数,错误就返回-1错误码被设置

当客户端使用recvfrom读取服务器返回发送的消息时——src_addr和addrlen没意义,但是还是要定义一个套接字类型结构体添上占位

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, 
                            (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

②sendto 向套接字发送信息

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

通过客户端的指定套接字sockfd,发送buf中的数据,buf的大小是len,flags=0 默认阻塞式发送,

dest_addr:(输入型参数)向哪个主机发消息,套接字类型指针struct sockaddr,传入的网络套接字类型struct sockaddr需要强转成此类型指针 struct sockaddr*。

addrlen:(输入型参数)主机这个缓冲区大小。(socklen t就是unsigned int)

返回值:返回读到的字节数,错误就返回-1错误码被设置

(首次调用sendto函数的时候,我们的client会自动bind自己的ip和port)

(5)日志写法(可变参数)

在C/C++中会遇到需要定义使用可变参数的函数,例如printf就是,他的格式就是int printf(const char *format,...),对于这样类型的函数,他的实现实际上就是从format格式的指针指向的空间中读取可变参数的类型,然后根据可变参数的首地址读取相应的可变参数值

   va_list ap;   va_start 就是char* 指针类型。

   void va_start(va_list ap, last);         va_start(ap, format);——获取可变参数的首地址并赋值给ap
    type va_arg(va_list ap, type);         ——提取ap,根据type参数类型获取实参值返回
    void va_end(va_list ap);                 ——将 ap 置空,即将可变参数指针归NULL
    void va_copy(va_list dest, va_list src);

int vsnprintf(char *str, size_t size, const char *format, va_list ap); 通过读取format得到可变参数的类型,将用户格式化的可变参数内容写入数组str中

str:把格式化内容写进str这个数组中。size:被写入空间的大小 sizeof(str)-1(不包含'\0')。format:存储 可变参数的类型 的空间。ap:可变部分

Log.hpp

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...) level日志等级
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);//用离可变参数format最近的参数初始化ap

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);

    va_end(ap); // ap = NULL

    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);

    // char *s = format;
    // while(s){
    //     case '%':
    //         if(*(s+1) == 'd')  int x = va_arg(ap, int);
    //     break;
    // }
}

4.部分细节解释+代码(udp套接字)

易错:1. port_ 端口号是一个 2字节16位的整数,主机转网络要用htos,不能用htol(这个错误找了一天呐~)server.sin_port=htons(server_port);

**htol **是转换四字节的,如果你传入一个两字节的数据,它就会自动进行补位,补位前面部分都是零,那这时候经过htol置换之后,前16位就变成零了,相当于你的程序跑去绑定零端口去了,就会绑定失败。

(1)INADDR_ANY

#define INADDR_ANY ((in_addr_t) 0x00000000)

local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());

①INADDR_ANY(这个宏的值就是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法(解释:一般服务器只有一个IP,会自动bind这个IP;如果服务器有多个IP,会自动bind这个服务器的所有的IP——因为如果有两个IP:IP1和IP2,只bind一个IP1,那么只有传给IP1的报文会交给程序,IP2就不会提交报文

云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 所以这里只能使用INADDR_ANY,如果你是虚拟机就可以bind自己虚拟机的IP,用ifconfig查看IP

(2)inet_addr(上面有)

in_addr_t inet_addr(const char *cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用。因为IP地址也是会发给对方的,所以除了做转化,inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)(INADDR_ANY 是0,所以h—>n转不转都行)

(3)bzero

bzero(&local,sizeof(1ocal)); ——bzero函数将从s开始的区域的前n个字节设置为0(字节包含'\0'). 也可以用memset代替

(4)本地通信:127.0.0.11——本地环回—代表本主机

客户端发送消息到本地的网络协议栈,但是不发送到网络,仅通过本地网络协议栈向上交付给另一个进程的缓冲区中。

服务器创建dup的流程:

服务器:创建套接字,填充信息,bind绑定,recvfrom等待接收消息,checkOnlineUser 添加在线用户,messageRoute 消息路由

客户端:创建套接字,填充服务器的信息,创建线程去recvfrom等待路由消息,主线程发消息给服务器

Makefile

.PHONY:all
all:udpClient udpServer

udpClient: udpClient.cc
    g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
    g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
    rm -f udpClient udpServer

udpClient.cc

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

struct sockaddr_in server;

static void Usage(std::string name)
{
    std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);//这个temp套接字结构体在这里不接收任何信息,只占位参数
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1. 根据命令行,设置要访问的服务器IP
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    // 2. 创建客户端
    // 2.1 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

// 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
// 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
(OS随机申请生成一个进程并让这个进程去绑定运行客户端)
// 如果我非要自己bind呢?可以!严重不推荐!
// 所有的客户端软件 <-> 服务器 通信的时候,必须得有client[ip:port]<->server[ip:port]
// 为什么不需要用户自己bind端口信息呢??client很多,不能给客户端bind指定的port,port
可能被别的client使用了,你的client就无法启动了
// 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!
server的端口号必须确定,但是客户端的端口号是多少不重要,因为没人连你的客户端,是你
连别人的服务器

    // 2.2 填写服务器对应的信息

    bzero(&server, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    pthread_t t;
    pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
    // 3. 通讯过程
    std::string buffer;
    while (true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);
        // 发送消息给server
        sendto(sockfd, buffer.c_str(), buffer.size(), 0,
               (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
    }

    close(sockfd);

    return 0;
}

udpServer.cc

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

#include "Log.hpp"

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}

/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:
    UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
    {
    }
    ~UdpServer()
    {
    }

public:
    void init()
    {
        // 1. 创建socket套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        logMessage(DEBUG, "socket create success: %d", sockfd_);
        // 2. 绑定网络信息,指明ip+port
        // 2.1 先填充基本信息到 struct sockaddr_in
        struct sockaddr_in local;     // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
        bzero(&local, sizeof(local)); // 可以用memset代替
        // 填充协议家族,域,选择是网络通信还是本地通信
        local.sin_family = AF_INET; sin_family就是开头的16位地址类型:AF_ INET
        // 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
        local.sin_port = htons(port_); port_类内成员是本地序列,要用htons转网络序列
        // 服务器都必须具有IP地址,42.192.83.143 "xx.yy.zz.aaa" ,字符串风格点分十进制 -> 4字节IP 
-> uint32_t ip(每个数字是0~255,8bit)
        // INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
        // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
        local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
        // 2.2 bind 网络信息
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
        {
            logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
            exit(2);
        }
        logMessage(DEBUG, "socket bind success: %d", sockfd_);
        // done
    }

    void start()
    {
        // 服务器设计的时候,服务器都是死循环
        char inbuffer[1024];  //将来读取到的数据,都放在这里
        char outbuffer[1024]; //将来发送的数据,都放在这里
        while (true)
        {
            struct sockaddr_in peer;      //输出型参数
            socklen_t len = sizeof(peer); //输入输出型参数

            // demo2
            //  UDP无连接的
            //  对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                //'\0'的值就是0,'0'的值是48,这里是存ASCII为0的'\0'
                inbuffer[s] = 0; //当做字符串
            }
            else if (s == -1)
            {
                logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);
                continue;
            }
            // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
            std::string peerIp = inet_ntoa(peer.sin_addr);  //拿到了对方的IP,因为inet_ntoa这个函数参数类型
就是in_addr而不是in_addr_t,所以参数填peer.sin_addr而不是peer.sin_addr.s_addr
            uint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的port

            checkOnlineUser(peerIp, peerPort, peer); //如果存在,什么都不做,如果不存在,就添加

            // 打印出来客户端给服务器发送过来的消息
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);

            // for(int i = 0; i < strlen(inbuffer); i++)
            // {
            //     if(isalpha(inbuffer[i]) && islower(inbuffer[i])) outbuffer[i] = toupper(inbuffer[i]);
            //     else outbuffer[i] = toupper(inbuffer[i]);
            // }
            messageRoute(peerIp, peerPort,inbuffer); //消息路由

            // 线程池!

            // sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);

            // demo1
            // logMessage(NOTICE, "server 提供 service 中....");
            // sleep(1);
        }
    }

    void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
    {
        std::string key = ip;
        key += ":";
        key += std::to_string(port);
        auto iter = users.find(key);
        if(iter == users.end())
        {
            users.insert({key, peer});
        }
        else
        {
            // iter->first, iter->second->
            // do nothing
        }
    }

    void messageRoute(std::string ip, uint32_t port, std::string info)
    {

        std::string message = "[";
        message += ip;
        message += ":";
        message += std::to_string(port);
        message += "]# ";
        message += info;

        for(auto &user : users)
        {
sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
        }
    }
            
private:
    // 服务器必须得有端口号信息
    uint16_t port_;
    // 服务器必须得有ip地址
    std::string ip_;
    // 服务器的socket fd信息
    int sockfd_;
    // onlineuser
    std::unordered_map<std::string, struct sockaddr_in> users;
};

// struct client{
//     struct sockaddr_in peer;
//     uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }

// ./udpServer port [ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }

    UdpServer svr(port, ip);
    svr.init();
    svr.start();

    return 0;
}

// struct ip
// {
//     uint32_t part1:8;
//     uint32_t part2:8;
//     uint32_t part3:8;
//     uint32_t part4:8;
// }
// struct ip ip_;
// ip_.part1 = s.substr();

5.linux上的联网通信 步骤(udp套接字)

①makefile改成静态编译

②sz udpClient 把客户端发送到桌面

相当于发布软件

③ rz -e 用户下载软件

④chmod +x udpClient 将程序转为可执行程序

⑤在linux上通信可以开始了

打开服务器 ./udpServer,此时服务器阻塞等待有人发消息。各个客户端:./udpClient +服务器的公网IP+8080(端口号),就可以发消息通信了

6.windows做客户端,linux做服务器的联网通信 步骤(udp套接字)

①makefile改成静态编译

windows上的客户端代码框架

#pragma comment(lib, "ws2_32.lib")    // 需要包含的链接库
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>    // windows socket  2.2版本

int main()
{
    WSADATA        wsaData;    // 用作初始化套接字   
    WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化启动信息(初始套接字)

    客户端的创建套接字,填充服务器的信息,sendto通信

    closesocket(SendingSocket);    // 释放套接字
    WSACleanup();        // 清空启动信息  
    system("pause");
    return 0;
}

全代码

发送这里的可执行程序就可以通信了

标签: 网络 linux

本文转载自: https://blog.csdn.net/zhang_si_hang/article/details/128142952
版权归原作者 beyond.myself 所有, 如有侵权,请联系我们删除。

“linux篇【12】:计算机网络<前序>—网络基础+udp套接字”的评论:

还没有评论