文章目录
温馨提示:文章较长(代码较多),请收藏起来慢慢观看。
一、再识网络
1. 端口号
在上文我们大致理解了,网络传输的基本流程和相关概念,知道ip地址可以标识唯一的一台主机
,但是主机获取到信息的最终目的是为了呈现给上层的用户,即我们所熟知的抖音等APP,既然有很多的APP,具体给哪一个APP呢?
- 说明:
- APP,具体指的是运行起来的程序,即一个一个的进程。
- 网络通信的本质是进程之间借助网卡(共享资源)进行通信。
- 概念
- 端口号:
- 指的是用于标记进程或者服务的逻辑地址。
- 范围为0 到 65535,分有大致三类:
- 系统端口:系统端口范围是从 0 到 1023,这些端口通常被一些众所周知的网络服务占用,比如 HTTP(端口 80)、HTTPS(端口 443)、FTP(端口 21)、SSH(端口 22)等。
通常需要root权限才能够进行使用。
- 注册端口:注册端口范围是从 1024 到 49151,这一范围的端口通常被一些应用程序或服务占用。
普通用户也可进行使用
。- 动态/私有端口:动态/私有端口范围是从 49152 到 65535,也被称为私有端口或暂时端口。这些端口通常被
客户端应用程序使用,用于临时通信。
疑问:既然进程的pid能标识唯一的进程,那为什么不直接捡现成的用呢?
答: 进程pid VS
端口号:
- 从概念上看:
- pid:操作系统管理进程使用。
- 端口号:网络通信以及为应用程序提供服务。
- 两者实现的解耦合的关系。
- 从使用形式上看:
- pid: 进程创建时才拥有。
- 端口号:固定一段范围,0 到 65535。
- 从用法来看:
- pid: 一个进程只能有一个pid。
- 端口号:一个进程能有多个端口号,为用户提供不同的服务。
- 联系:pid和端口号都是只能对应一个进程。且通过端口号可找到进程pid,从而找到进程。
- 总结:
- 通过IP地址标识唯一的一台主机。
- 通过端口号标识唯一的一个进程。
- 进而我们可以实现网络之间的通信。
拓展:在实际进行通信的过程中,一般是由客户端访问服务器,由服务器再提供对应的服务。
- 说明:
- 客户端要想访问服务器,首先得知道服务器的ip地址和对应服务的端口号。这些工作早已经由开发人员做好,因此无需担心。
- 服务器的ip地址和端口号一般是不能发生变化的,否则客户端就无法访问。因为客户端的载入的服务器的端口号和ip一般是固定的。
- 客户端的端口号是动态变化的。这是因为多个app的开发厂商并不互通,因此可能存在端口号冲突的现象,因此要动态绑定端口号,而且这样做更加灵活,安全,高效。
- 服务器要对大量用户提供服务,而且用户的IP地址是随机变化的,这也间接的导致了,服务器要在"客户端做一些手脚", 即固定服务器的ip地址和端口号。
2. 网络字节序列
关于数据用大端还是用小端,就跟鸡蛋先吃大头还是先吃小头一样,没有实际的争论意义,因此我们看到电脑既有大端机,也有小端机。
- 说明:big - endian 为大端机的数据,little - endian为小端机的数据。
- 速记:大同小异反着记——大 “异” 小 “同”。
-
但是网络里面传输数据,不可能即传输大端数据也传输小端,因此
规定统一在网络里面传输大端数据
,到对应的主机里面再进行统一的转换,大端不用变,小端再转换一下即可。 -
相关的接口:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- 速记:h(host) to n(net) l(long),即将long类型的数据从主机转网络。其余类似。
3.TCP 与 UDP
传输方式:
- TCP:面向字节流。
- UDP:面向数据报。
这是最本质的差别,下面我们进行分析:
- 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送,接收方按照接收到的顺序重新组装数据。
- 提供了可靠的传输,保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。
- 基于连接的通信方式,需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销,但可以确保数据的有序传输。
- 适用于需要可靠传输和有序性的应用,如文件传输、视频流传输等。
总结:
TCP协议,面向字节流,可靠,有连接。适合文件和视频等信息的传输。
- 将数据划分为独立的数据,即数据报,每个数据报都携带了完整的信息,可以独立地发送和接收。
- 不保证数据的可靠性,每个数据报都是独立传输的,可能会发生丢失、重复或乱序。
- 无连接的通信方式,每个数据报都是独立的,不需要事先建立连接。
- 对实时性要求较高的应用,如实时音频、视频通信等,因为它可以提供更低的延迟。
总结:
UDP协议,面向数据报,不可靠,无连接。适用于对实时性要求高的应用。
- 说明:这里的可靠和不可靠是一个中性词。不可靠意味着较低的成本,实现更加简单,可靠意味着实现需要较大的代价。因此没有谁好谁坏。
下面我们实现是更为简单的UDP套接字。
- 在开始之前我们先来解决一个前置问题,主要是服务器的端口问题,一般默认有些端口是禁掉的,不能用于网络之间的通信,因此我们需要开放一些端口供我们之间通信使用。
- 实现步骤:
- 登录所在云服务的官网。(我的是阿里云的)
- 点击控制台。
- 点击云服务器ESC/轻量级服务器/云服务器,找到对应的云服务器。(我的是轻量级云服务器)
- 如果是云服务器ESC/服务器就找到安全组,点击安全组ID进行编辑即可。如果是轻量级服务器就在服务器一栏找到实例id点击,再点击防火墙进行编辑即可。
- 具体步骤——阿里云轻量级云服务器
- 第一步:
- 第二步:
- 第三步:
- 第四步:
二、套接字
1.sockaddr结构
- 这是一层
抽象
化的结构,设计之初是为了统一网络套接字的接口使用
,是一套通用的网络套接字,而对应的具体
的套接字有网络套接字 与 域间套接字
。
图解:
-
类似多态的思想,即从抽象到具体。在使用过程中我们可以通过传入通用的套接字类型,并且指定对应的套接字大小,从而说明其对应的具体类型,也就是我们说的多态。
-
我们实现的是网络编程,使用的是:
struct sockaddr_in
。
- 具体结构:
- sin_family_t sin_family; 所属家族协议类型,一般设置为AF_INT/PF_INT,即ipv4类型的协议。
- in_port_t sin_port; 端口号。
- struct in_addr sin_addr; ip地址。
- 注意:端口号和ip地址的数据都为网络序列。
2.UDP
- Log.hpp(记录日志信息)
#pragma once
#include<map>
#include<iostream>
#include<cstdio>
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<time.h>
using namespace std;
#define SIZE (4096)
//事件的等级
#define EMRGE 1
#define ALERK 2
#define CRIT 3
#define ERRO 4
#define WARNNING 5
#define NOTICE 6
#define INFORE 7
#define DEBUG 8
#define NONE 9
//输出方向
#define DEFAULTFILE 1
#define CLASSFILE 2
#define SCREAN 0
//说明:一般我们在传参时一般都是以宏的方式进行传参的,
//如果需要打印出字符串可以用KV类型进行映射转换。
map<int,string> Mode = {
{
1,"EMERG"},{
2,"ALERK"},{
3,"CRIT"},
{
4,"ERRO"},{
5,"WARNING"},{
6,"NOTICE"},
{
7,"INFOR"},{
8,"DEBUG"},{
9,"NONE"}
};
//分类文件处理的后缀。
map<int,string> file = {
{
1,"emerg"},{
2,"alerk"},{
3,"crit"},
{
4,"erro"},{
5,"warning"},{
6,"notice"},
{
7,"infor"},{
8,"debug"},{
9,"none"}
};
class Log
{
public:
void operator()(int level,const char* format,...)
{
//将输入的字符串信息进行输出。
va_list arg;
va_start(arg,format);
char buf[SIZE];
vsnprintf(buf,SIZE,format,arg);
va_end(arg);
//获取时间
time_t date = time(NULL);
struct tm* t = localtime((const time_t *)&date);
char cur_time[SIZE] = {
0};
snprintf(cur_time,SIZE,"[%d-%d-%d %d:%d:%d]",\
t->tm_year + 1900,t->tm_mon + 1,
t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
//输入再进行合并
string Log = "[" + Mode[level] + "]" + \
cur_time + string(buf) + "\n";
//处理输出方向
PrintClassFile(level,where,Log);
}
void PrintDefaultFILE(string& file_name,const string& mes)
{
int fd = open(file_name.c_str(),O_CREAT | O_WRONLY \
| O_APPEND,0666);
write(fd,mes.c_str(),mes.size());
close(fd);
}
//将文件进行分类进行输出。
void PrintClassFile(int level,int where,const string& mes)
{
if(where == SCREAN)
cout << mes;
else
{
string file_name = "./log.txt";
if(where == CLASSFILE)
file_name += ("." + file[level]);
PrintDefaultFILE(file_name,mes);
}
}
void ReDirect(int wh)
{
where = wh;
}
private:
int where = SCREAN;
};
说明:在【Linux进阶之路】进程间通信有所提及,具体这个小组件是用来帮助我们显示出日志的时间,等级,出错内容等信息。
1.server端
- 基本框架:
//所用容器
#include<string>
#include<unordered_map>
//与内存相关的头文件
#include<string.h>
#include<strings.h>
//网络相关的头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//包装器
#include<functional>
//日志头文件
#include "Log.hpp"
//枚举常量,用于失败退出进程的退出码
enum
{
SOCKET_CREAT_FAIL = 1,
SOCKET_BIND_FAIL,
};
class UdpServer
{
public:
UdpServer(uint16_t port,string ip)
:_port(port),_ip(ip),_sockfd(0)
{
}
~UdpServer()
{
}
void Init()
{
}
void Run()
{
}
private:
int _sockfd;
string _ip;
uint16_t _port;
};
1.1 构造函数
- 一般我们使用1024以上的端口号即可,此处我们默认使用8080端口。
- 云服务器,禁止直接绑定公网ip。
- . 解决方法——因此我们绑定的时候使用0.0.0.0即任意ip地址绑定即可,即接收所有云服务器地址的发来的信息。
- . 方法优点——服务可以在服务器上的所有IP地址和网络接口上进行监听,从而提供更广泛的访问范围。
- 因此在构造函数里,我们给出两个缺省值即可。
//全局定义:
uint16_t default_port = 8080;
string default_string = "0.0.0.0";
//类内
UdpServer(uint16_t port = default_port,string ip = default_string)
:_port(port),_ip(ip),_sockfd(0)
{
}
1.2 Init
- 创建套接字
- 接口
//头文件:
#include <sys/types.h>
#include <sys/socket.h>
//函数声明:
int socket(int domain, int type, int protocol);
/*
参数:
1:指定通信域,使用AF_INT即可,即IPV4的ip地址。
2: SOCKET_DGRAM,即使用的套接字类型,指的是UDP类型的套接字。
3: 指定协议,一般设为0,根据前两个参数系统会自动选择合适的协议。
返回值:
1.成功返回对应的文件描述符,网络对应的是网卡文件。
2.失败返回-1。
*/
- 绑定套接字
- 接口:
//头文件:
#include <sys/types.h>
#include <sys/socket.h>
//函数声明:
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
/*
参数:
1.网络的文件描述符。
2.sockaddr具体对象对应的地址,为输入型参数。
3.具体对象对应的大小,为输入型参数。
说明:在传参之前,sockaddr对象应初始化完成。
返回值:
1.成功返回 0。
2.失败返回 -1,并设置合适的错误码。
*/
- 说明:在传入sockaddr具体对象对应的地址时,需要再强转为sockaddr*类型的,因为也传进去了具体对象的大小,所以内部会再识别出具体的对象,再进行处理。
-
这里的IP地址的形式为字符串类型的,便于用户进行识别,而在网络当中是usiged int 类型的,中间需要转换一下。
-
实现代码:
#include<string>
#include<iostream>
using std::string;
using std::cout;
using std::endl;
struct StrToIp
{
unsigned int str_to_ip(const string& str)
{
int ssz = str.size();
int begin = 0;
int index = 3;
for (int i = 0; i <= ssz; i++)
{
if (str[i] == '.' || i == ssz)
{
string tmp = str.substr(begin,i);
begin = i + 1;
unsigned char n = stoi(tmp);
if (index < 0) return 0;
part[index--] = n;
}
}
//auto p = (unsigned char*)&ip;
//for (int i = 0; i < 4; i++)
//{
// *(p + i) = part[i];
//}
//return ip;
return *((unsigned int*)part);
}
unsigned char part[4] = {
0 };
//unsigned int ip = 0;
};
int main()
{
StrToIp s;
cout << s.str_to_ip("59.110.171.160") << endl;
return 0;
}
- 我们将字符串分为四部分,然后转换为char类型的四个变量,存储即可。
- 这四个部分我们存放在数组或者单独存都可以,这里我采用数组便于操作。
- 如果为数组,具体转换为int变量时,应注意四个部分的存储顺序。
- 运行结果:
- 说明:
- 我所在的机器为小端机,数据是低位放在低地址处,所以应该倒着存每一段。
- 如果为大端机,数据是高位放在低地址处,所以应该正着存每一段。
- 最后强转取数据即可。
- 补充: 指针指向的是对象的低地址处。
在实际编程的过程中,相应的接口已经准备好,不需要手动的写,但相应的原理还是要清楚的。
- 字符串转地址的网络序列接口:
//头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
/*
参数:
1.转化的ip地址的字符串。
2.输出型参数,in_addr的变量。
返回值:
1.成功返回非零值,通常为1.
2.失败返回零值。
*/
in_addr_t inet_addr(const char *cp);
/*
参数:
1.转化的ip地址的字符串。
返回值:
1.成功返回对应的ip值。
2.失败返回INADDR_NONE,其定义为 (in_addr_t) -1。
*/
- 主机ip地址转字符串的接口:
//头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
/*
参数:存放主机序列的ip地址
返回值:字符串形式的ip。
*/
- 实现代码:
void Init()
{
//1.创建套接字,即创建文件描述符
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd < 0)
{
lg(CRIT,"socket fail,error message is %s,error \
number is %d ",strerror(errno),errno);
exit(SOCKET_CREAT_FAIL);
}
lg(INFORE,"socket fd is %d,socket success!",_sockfd);
//2.绑定套接字
/*
注意:主机序列都要转成网络序列。
*/
// 2.1初始化域间套接字
struct sockaddr_in server_mes;
bzero(&server_mes,sizeof(server_mes));
server_mes.sin_family = AF_INET;
server_mes.sin_port = htons(_port);
server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
socklen_t len = sizeof(server_mes);
// server_mes.sin_addr.s_addr = INADDR_ANY;;
//任意地址转网络序列
// 2.2 绑定域间套接字
int ret = bind(_sockfd,(const sockaddr*)&server_mes,len);
if(ret < 0)
{
lg(CRIT,"bind fail,error message is %s,\
error number is %d ",strerror(errno),errno);
exit(SOCKET_BIND_FAIL);
}
lg(INFORE,"ret is %d,bind success!",ret);
}
1.3 Run
- 等待客户发信息
接口
//头文件
#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);
/*
参数:
1.文件描述符。
2.缓存区,读取用户发来的信息。
3.缓存区的大小。
4.一般使用默认值0即可。
5.src_addr变量的地址,用于接收用户的网络信息,输出型参数。
6.addrlen用于接受用户的src_addr具体对象的长度,输入输出型参数。
返回值:
1.成功,返回接受的字节个数。
2.连接关闭,返回0。
3.错误返回-1.设置合适的错误码。
*/
- 给客户提供服务
接口:
//头文件:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)
参数:
1.文件描述符。
2.缓存区,存放发给用户的信息。
3.缓存区的大小。
4.一般使用默认值0即可。
5.src_addr变量的地址,用于存放用户的网络信息。
6.addrlen用于存放用户的src_addr具体对象的长度。
返回值:
1.成功,返回实际发送的字节个数。
3.错误返回-1.设置合适的错误码。
- 实现代码:
void Run()
{
for(;;)
{
//存放用户消息的缓存区
char buffer[1024] = {
0};
//用于存放用户的网络信息
struct sockaddr_in client_mes;
socklen_t len;
//收消息
ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1\
,0,(sockaddr*)&client_mes,&len);
if(n < 0)
{
lg(WARNNING,"recvfrom message fail,waring \
message is %s",strerror(errno));
continue;<