文章目录
- 1. 网络基础
- 1.1 网络协议
- 1.1.1 OSI七层模型
- 1.3 网络中的地址管理
- 2. 套接字编程
- 2.1 源IP地址和目的IP地址
- 2.3 socket编程接口
- 2.3.2 sockaddr结构
- 2.2.3 UDPecho服务器
- 2.24 netstat
- 2.25 远程执行命令
1. 网络基础
1.1 网络协议
1.1.1 OSI七层模型
- OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范;
- 把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机;
- OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;
- 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七 个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯;
- 但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来讲解.
层级 | 名称 | 功能 |
---|---|---|
第七层 | 应用层 | 提供用户接口,处理用户请求和数据传输。 |
第六层 | 表示层 | 处理数据的表示和转换,确保传输的数据格式统一。 |
第五层 | 会话层 | 管理通信会话,确保数据的传输顺序和完整性。 |
第四层 | 传输层 | 负责端到端的数据传输,提供可靠的数据传输服务。 |
第三层 | 网络层 | 处理数据的路由和转发,实现不同网络之间的通信。 |
第二层 | 数据链路层 | 控制数据在物理介质上的传输,管理节点之间的数据流动。 |
第一层 | 物理层 | 管理物理介质,负责将比特流转换为适合传输的信号。 |
###1.1.2 TCP/IP五层(或四层)模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求.
- 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.
- 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.
- 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.
- 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.
- 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层.
物理层我们考虑的比较少. 因此很多时候也可以称为 TCP/IP四层模型
一般而言
- 对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容;
- 对于一台路由器, 它实现了从网络层到物理层;
- 对于一台交换机, 它实现了从数据链路层到物理层;
- 对于集线器, 它只实现了物理层;
但是并不绝对. 很多交换机也实现了网络层的转发; 很多路由器也实现了部分传输层的内容(比如端口转发);
##1.2 网络传输基本流程
网络传输流程图
同一个网段内的两台主机进行文件传输.
应用层:Telnet、FTP和e-mail等
传输层:TCP和UDP
网络层:IP、ICMP和IGMP
链路层:设备驱动程序及接口卡
数据包封装和分用
- 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame).
- 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装 (Encapsulation).
- 首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息.
- 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理.
下图为数据封装的过程
下图为数据分用的过程
1.3 网络中的地址管理
-
IP地址(Internet Protocol Address):IP地址是用于在Internet上唯一标识设备(如计算机、路由器等)的地址。它是一组数字,通常以四个十进制数(例如,192.168.1.1)表示,每个数的取值范围为0到255。IP地址分为IPv4和IPv6两种版本,IPv4是目前广泛使用的版本,而IPv6则是为了解决IPv4地址短缺问题而设计的新版本。
IP地址用于在网络中路由数据包,确保它们能够从源设备传输到目标设备。它们也可以用于识别设备所在的网络和子网络。
IPv6地址并不是六个十进制数,而是由8组16位的十六进制数构成,每组之间用冒号分隔,例如:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
每组的十六进制数表示一个16位的段,共128位,这样IPv6地址的地址空间远远大于IPv4地址空间,因此可以为更多的设备提供唯一的标识。
-
MAC地址(Media Access Control Address):MAC地址是网络设备(如网卡、无线网卡等)在出厂时分配的唯一标识符。它是一个48位的十六进制数,通常以6个字节的形式表示,如00:1A:2B:3C:4D:5E。MAC地址是设备的固定地址,与设备的网络连接硬件紧密相关。
MAC地址用于在局域网(LAN)中唯一标识设备。当数据包在局域网中传输时,路由器或交换机会使用MAC地址来决定将数据包发送到哪个设备。
在网卡出厂时就确定了, 不能修改. mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址, 可能会冲突; 也有些网卡支持用户配置mac地址). 比特
2. 套接字编程
2.1 源IP地址和目的IP地址
IP数据包头部包含了源IP地址和目的IP地址,它们分别指示了数据包的发送者和接收者
##2.2 端口号
端口号是网络通信中的一个重要概念,用于标识在网络连接中的特定应用程序或服务。
在TCP/IP协议中,端口号是一个16位的数字,取值范围是0到65535。端口号分为两种类型:系统端口和动态端口。
- 系统端口:系统端口是预留给一些常用的网络服务的端口号,例如HTTP服务的端口号是80,HTTPS服务的端口号是443,FTP服务的端口号是21等。这些端口号的范围是0到1023,也被称为“众所周知的端口”。
- 动态端口:动态端口是由操作系统随机分配给客户端应用程序的端口号,用于临时通信。它们的范围通常是从1024到65535。
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用.
“端口号” 和 “进程ID”
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要 发给谁”;
目标端口号通常是由服务或应用程序提前确定好的,而不是在通信过程中动态选择的。
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。
大端字节序与人类读写数值的方式更加一致,因此在网络通信中被广泛采用。
(记忆方法:小小小)
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
htonl()
- 将32位主机字节序转换为网络字节序(大端字节序)。htons()
- 将16位主机字节序转换为网络字节序(大端字节序)。ntohl()
- 将32位网络字节序(大端字节序)转换为主机字节序。ntohs()
- 将16位网络字节序(大端字节序)转换为主机字节序。
这些函数通常在 <arpa/inet.h>
头文件中声明。使用这些函数可以确保在不同字节序的计算机上,通过网络传输的数据始终以大端字节序进行传输和解析。
2.3 socket编程接口
###2.3.1 socket 常见API
C 语言中的 socket 编程接口通常使用 POSIX 标准库中的 socket()
、bind()
、listen()
、accept()
、connect()
和 recv()
、send()
等函数来实现。这些函数通常用于创建网络套接字、绑定地址、监听连接、接受连接、建立连接以及发送和接收数据等操作。以下是一些常见的 socket 编程接口函数及其作用:
-
socket():创建一个新的套接字。
int socket(int domain, int type, int protocol);
domain
参数指定了通信的协议族,常见的包括AF_INET
(IPv4)和AF_INET6
(IPv6)等。type
参数指定了套接字的类型,常见的包括SOCK_STREAM
(流套接字,如 TCP)和SOCK_DGRAM
(数据报套接字,如 UDP)等。protocol
参数指定了使用的协议,通常设为 0,让系统根据domain
和type
自动选择合适的协议。
-
bind():将套接字绑定到一个地址上。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
listen():监听传入的连接。
int listen(int sockfd, int backlog);
-
accept():接受传入的连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
connect():建立与远程主机的连接。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
recv() 和 send():接收和发送数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
这些函数都在 <sys/socket.h>
和 <netinet/in.h>
(对于网络编程)中声明。在使用这些函数之前,通常需要创建套接字,并根据需要设置地址、端口等信息。此外,在网络编程中,还需要处理地址结构体 struct sockaddr
和 struct sockaddr_in
(对于 IPv4 地址)等。
2.3.2 sockaddr结构
sockaddr
结构体是用于表示通用套接字地址的结构体,它在网络编程中被广泛使用。在 C 语言中,sockaddr
结构体用于表示各种协议族(如 IPv4、IPv6)的套接字地址信息,使得网络编程可以在不同的协议族之间通用。
以下是 sockaddr
结构体的定义:
struct sockaddr {sa_family_t sa_family; // 协议族(地址族)char sa_data[14]; // 地址数据,具体内容取决于协议族
};
这里的 sa_family
是一个无符号短整型,用于指定地址的协议族,它的值可以是 AF_INET
(IPv4)、AF_INET6
(IPv6)等,具体取值取决于所使用的协议族。而 sa_data
数组则用于存储地址的具体数据,其长度是 14 字节,但具体的内容和长度取决于使用的协议族。
在实际的网络编程中,通常使用的是 sockaddr_in
结构体来表示 IPv4 地址,它是 sockaddr
的一个特定形式。sockaddr_in
结构体的定义如下:
struct sockaddr_in {sa_family_t sin_family; // 协议族(地址族)in_port_t sin_port; // 端口号struct in_addr sin_addr; // IPv4 地址char sin_zero[8]; // 为了使 sockaddr_in 和 sockaddr 兼容而保留的空字节
};
在 sockaddr_in
结构体中,除了协议族之外,还包含了端口号和 IPv4 地址。其中 sin_port
是一个无符号短整型,用于存储端口号,sin_addr
则是一个 struct in_addr
结构体,用于存储 IPv4 地址。sin_zero
数组用于填充,使 sockaddr_in
结构体的大小和 sockaddr
结构体的大小一致,保证了在函数调用时可以进行类型转换。
- 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结构体指针做为 参数;
2.2.3 UDPecho服务器
//main.cc
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>// "120.78.126.148" 点分十进制字符串风格的IP地址void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;// pid_t id = fork();// if(id == 0)// {// // ls -a -l -> "ls" "-a" "-l"// // exec*();// }return res;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);std::cout<<"初始化完成"<<std::endl;svr->Run(Handler);return 0;
}
//UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);std::cout << message << std::endl;// 1. 数据 2. 给谁发sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);std::cout << "send success" << std::endl;struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);std::cout << "receive success" << std::endl;if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
//UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct 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()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void Run(func_t func) // 对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);std::cout<<"收到成功"<<std::endl;if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;std::string info = inbuffer;std::string echo_string = func(info);sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_; // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;
};
INADDR_ANY
在套接字编程中,调用 bind()
函数用于将一个特定的 IP 地址和端口号绑定到一个套接字上。当你使用 bind()
函数时,可以选择绑定到特定的 IP 地址和端口号,也可以选择绑定到特定的端口号而将 IP 地址留空,还可以选择将 IP 地址和端口号都留空。
当你调用 bind()
函数并将 IP 地址参数设为 0(通常表示 INADDR_ANY
),这意味着你希望套接字可以接受来自任意 IP 地址的连接。换句话说,它会监听所有网络接口上的连接请求。这样做的典型场景是在服务器程序中,当你想要在主机的所有可用 IP 地址上监听某个端口时。
2.24 netstat
-
netstat -naup
:-
netstat
是网络统计(network statistics)的缩写,用于显示网络连接、路由表和网络接口信息。 -
-naup
是参数,每个字母都代表不同的选项:
-n
表示不要解析服务名称,而是显示数字形式的地址和端口号。-a
表示显示所有的连接和侦听端口,而不仅仅是活动的连接。-u
表示只显示 UDP 协议的连接。-p
表示显示建立相关的进程/程序。
-
所以,
netstat -naup
命令将显示所有的 UDP 连接以及它们对应的端口号和相关进程信息。
-
-
netstat -natp
命令来只显示 TCP 连接并包含相关进程信息。 -
netstat -nalp
显示所有协议(TCP、UDP、ICMP 等)的连接。
2.25 远程执行命令
bool SafeCheck(const std::string &cmd)
{int safe = false;std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(auto &word : key_word){auto pos = cmd.find(word);if(pos != std::string::npos) return false;}return true;
}std::string ExcuteCommand(const std::string &cmd)
{std::cout << "get a request cmd: " << cmd << std::endl;if(!SafeCheck(cmd)) return "Bad man";FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}
###2.26 远程执行命令
###2.27 windows客户端
###2.28 聊天室,转发,多线程
多终端,命令行重定向