学习任务:
我们先来认识端口号,区分好主机IP和端口号的区别,以及涉及到进程PID和端口号的区别。
然后简单认识一下TCP协议和UDP协议,这两个协议都是传输层的。接着了解什么是网络字节序,它有什么作用。然后是网络编程的一些接口。最后写代码简单实践一下。
目录
1、认识端口号,区分IP/port,PID/port
2、认识TCP协议,认识UDP协议
3、认识网络字节序
4、socket编程接口
5、代码示例:能够实现一个简单的udp客户端/服务器
1、认识端口号,区分IP/port,PID/port
IP地址(公网IP)是用来唯一标识互联网中的一台主机,一台主机一个IP。而IP分源IP和目的IP,源IP和目的IP对一个报文来讲,是起从哪里来,到哪里去的作用,其最大的意义是指导报文该如何进行路径的选择,而路径中,每一个“站点”就是MAC地址的变化。
认识端口号port
数据从计算机A到达计算机B,并不是真正的目的,而是到达计算机B的某一个进程,提供数据处理的服务,才是网络传输数据最终的目的。
数据本身并不是由计算机产生的,而是由人,即用户通过特定的客户端等等输入进去的,因此本质上,所有的网络通信,站在人的角度上,就是人与人之间的通信,这是一个比较好的理解方向,站在计算机角度上,是进程间通信!只不过通信的进程不在一台计算机上。就比如抖音的app客户端,它是一个进程,抖音的服务器,也是一个进程。我们通过抖音客户端达到网络通信,在抖音的服务器上获取信息,便是进程间通信。
而IP地址,仅仅是解决了两台物理机器之间的相互通信的识别问题,我们还要解决是在这两台计算机之间的进程间的通信,就是怎么知道计算机A发出的信息是要传给计算机B中的某个进程呢?这就需要端口号了!
因此,端口号的作用是唯一标识一台机器上的唯一一个进程!通过IP+端口号port,就能够标识互联网中的唯一一个进程!
我们可以将整个网络看成是一个大的OS,所有的网络行为,几乎都是在这一个大的OS进行进程间通信!
既然说端口号port是进程的一个身份,那么进程的PID按理论上来说,也能通过PID来进行网络上的进程间通信,那么为什么还需要一个port呢?
区分IP/PORT,PID/PORT
上面我们已经很清楚了,IP的标识物理机器的,port是标识进程的。而PID也是用来标识进程的,也是唯一性的!其实PID跟port,都属于进程的身份,就好像学生由身份证,也有他的学生证,一句话来说,将进程的PID和port分开来使用,是为了解耦!
一个进程可以关联多个端口号,而一个端口号不能关联多个进程。
网络是一份共享资源
要在网络上进行进程间通信,我们首先需要找到目标主机,然后找到该主机上的服务(进程),完成进程间通信。我们可以说网络世界,是一个进程间通信的世界。而进程要通信的话,由于进程具有独立性,因此不同的进程必须看到同一份资源,即共享资源!所有,网络便是一份共享资源!
2、认识TCP协议,认识UDP协议
这里先简单得对TCP和UDP来一个直观的认识:
TCP协议和UDP协议都是传输层的控制协议,以下是两种协议的特定,我们需要根据它们的特定,在不同场景下,权衡使用哪种协议。
TCP协议:
*传输层协议 *有连接 *可靠传输 *面向字节流
YDP协议:
*传输层协议 *无连接 *不可靠传输 *面向数据报
3、认识网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
网络数据流觉得这样分来分去太麻烦了,这样吧!我就使用大端的形式吧!如果你的数据流本来就是大端,那你就直接传输,如果你的数据流是小端,那么麻烦你先转换成大端,再来传输!
因此,网络字节序指的就是在网络上的采用的大端形式,先发出的数据是低地址,后发出的数据是高地址。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
总结一下网络字节序:
⭐发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
⭐接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
⭐因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
⭐TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
⭐不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
⭐如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
4.socket编程接口
socket是套接字的意思,用于描述IP地址和端口号,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。
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);
我们逐一来理解一下这些接口:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
第一个参数:domain:协议域。就是需要用哪种协议,我们最常用的就两种->AF_INET (IPV4协议),AF_INET6 (IPV6协议)。
第二个参数:套接字的类型,即SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。
第三个参数:这个我们置为0即可,它是用来制定某个协议的特定类型,即type类型中的某个类型。通常一种协议只有一种类型,那样该参数可以直接被设置为0;如果协议有多种类型,则需要指定协议类型。
返回值:返回一个文件描述符。
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
第一个参数:socket函数返回的文件描述符。
第二个参数:指定想要绑定的IP和端口。下面将分析sockadder结构体。
第三个参数:address的长度。
返回值:成功为0,失败-1
sockaddr结构:
网络通信的方式有很多种,比如基于网IP的网络通信,AF_INET,原始套接字,域间套接字等等。有那么多方式,那么在绑定IP和端口的时候,就需要很多种方法了,因此系统需要将其统一一下结构,就有了sockadder。
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 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
我们简化看看表示IPV4的结构体:
struct sockaddr_in
{
sa_family_t sin_family;//地址族
uint16_t sin_port;//TCP/UDP端口号,16位整型
struct in_addr sin_addr;//IP地址,32位整型
char sin_sero[8];//别管它了
};
其中sin_famile:
地址族 | 含义 |
AF_INET | IPV4网络协议中的使用的地址族 |
AF_INET6 | IPV6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
我们使用这两个函数,再补充两个函数:recvfrom和sendto就可以写一个示例了(UDP的)。
recvfrom:适用于UDP协议
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址
第一个参数:套接字文件描述符
第二个参数:指明一个缓冲区,该缓冲区用来存放recvfrom函数接收到的数据
第三个参数:buf的长度
第四个参数:一般置0,即false。
第五个参数:是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。
第六个参数:第五个参数的sizeof
返回值:成功返回接收到的字节数。失败返回-1。
sendto:适用于UDP协议
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
·第一个参数:套接字文件描述符。
第二个参数:指明一个存放应用程序要发送数据的缓冲区。
第三个参数:buf的长度
第四个参数:置为0吧。
第五个参数:dest_addr表示目地机的IP地址和端口号信息
第六个参数:dest_addr的长度
返回值:成功返回接收到的字节数。失败返回-1。
示例代码:
实现一个网络通信功能,在client中输入信息,会在server中显示出来,并且返回信息给client,达到网络通信聊天的效果。
客户端client代码:
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout<<"Usage: \n\t"<<proc<<" server_ip server_port"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
return 0;
}
//1.创建套接字,打开网络文件
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
std::cerr << "socket error : " << errno << std::endl;
return 1;
}
//客户端不需要显示bind。
//首先,客户端必须也要有IP和port
//但是,客户端不需要显示的bind。因为一旦显示bind,就必须明确客户端client
//要和哪个端口port关联。
//而如果客户端client指明了端口号,那么在客户端client不一定会有用,因为
//这个端口号有可能被占用了,比如我们在联网的时候,一边打LOL,一边斗地主
//被占用就会导致client无法使用
//server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind()
// 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//使用服务
while(1)
{
//数据从我们键盘输入
std::string message;
std::cout<<"输入# ";
std::cin>>message;
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server, sizeof(server));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&tmp, &len);
std::cout<<"server echo# "<<buffer<<std::endl;
}
return 0;
}
服务器server代码:
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
const uint16_t port = 8080;
int main()
{
//1.创建套接字,打开网络文件
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
std::cerr<<"socket create error: "<<errno<<std::endl;
return 1;
}
//2.给该服务器绑定端口和ip
struct sockaddr_in local;
//填充字段
local.sin_family = AF_INET;
local.sin_port = htons(port);//此处的port是端口号,是计算机上的变量
//是属于主机序列,说明需要主机转网络的操作htons();
//需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP
//需要考虑大小端,因此使用in_addr_t inet_addr(const char *cp);
//local.sin_addr.s_addr = inet_addr("43.139.32.198");//点分十进制【0-255】
//我们不能像上面这行代码一样,直接绑定bind某个IP,因为如果指定绑定一个IP,那么
//只有发送到该IP主机上的数据才会交给你的网络进程
//但是,服务器一般会配置很多个网卡,有很多个IP。
//因此,作为服务器,我们需要的不是某个IP上面的数据,
//而是需要所有发送到该服务器主机上的某个端口的数据!
//使用INADDR_ANY,不绑定指定IP
local.sin_addr.s_addr = INADDR_ANY;
//绑定IP和端口
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
std::cerr<<"bind error: "<<errno<<std::endl;
return 2;
}
//3.提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while(!quit)
{
struct sockaddr_in peer;//保存接受到的数据的空间
socklen_t len = sizeof(peer);//空间的大小
recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
std::cout<<"client# "<<buffer<<std::endl;
std::string echo_hello = "hello";//服务器发送回给用户,表示收到消息了
sendto(sock,echo_hello.c_str(),echo_hello.size(),0,(struct sockaddr*)&peer,len);
}
return 0;
}
接下来我们改造一下代码,实现一个功能:在client中输入bash命令,在server中执行命令,并且将执行结果返回到客户端client中。实现了简单的xshell。
client的代码:
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 0;
}
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error : " << errno << std::endl;
return 1;
}
//客户端需要显示的bind的吗??
// a. 首先,客户端必须也要有ip和port
// b. 但是,客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联
// client指明的端口号,在client端一定会有吗??有可能被占用,被占用导致client无法使用
// server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind()
// 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
// b. 你要给谁发??
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
// 2.使用服务
while (1)
{
// a. 你的数据从哪里来??
// std::string message;
// std::cout << "输入# ";
// std::cin >> message;
std::cout<<"MyShell $ ";
char line[1024];
fgets(line,sizeof(line),stdin);
sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));
//此处tmp就是一个”占位符“
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
if(cnt>0)
{
//在网络通信中,只有报文大小,或者是字节流中字节的个数
//没有C/C++字符串这样的概念
buffer[cnt] = 0;//添加'\0'
std::cout << buffer << std::endl;
}
else
{
//TODO
}
}
return 0;
}
server的代码:
#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//const uint16_t port = 8080;
std::string Usage(std::string proc)
{
std::cout<<"Usage: "<<proc<<"port"<<std::endl;
}
// udp_server,细节最后在慢慢完善
// ./udp_server port
int main(int argc,char *argv[])
{
if(argc!=2)
{
Usage(argv[0]);
return -1;
}
uint16_t port = atoi(argv[1]);//argv[1]是字符串类型,需要转成整型
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0){
std::cerr << "socket create error: " << errno << std::endl;
return 1;
}
//2. 给该服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port); //此处的端口号,是我们计算机上的变量,是主机序列
// a. 需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP
// b. 也要考虑大小端
// in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作.
// 坑:
// 云服务器,不允许用户直接bind公网IP,另外, 实际正常编写的时候,我们也不会指明IP
// local.sin_addr.s_addr = inet_addr("42.192.83.143"); //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
// INADDR_ANY: 如果你bind的是确定的IP(主机), 意味着只有发到该IP主机上面的数据
// 才会交给你的网络进程, 但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是
// 某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error : " << errno << std::endl;
return 2;
}
//3. 提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while(!quit)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(cnt>0)
{
buffer[cnt] = 0;//0=='\0'
FILE *fp = popen(buffer,"r");
std::string echo_hello;
char line[1024] = {0};
while(fgets(line,sizeof(line),fp)!=NULL)
{
echo_hello+=line;
}
//这里可以选择判断是否读到了文件结尾
// if(feof(fp))
// {
// //读取结果完成
// }
pclose(fp);
std::cout << "client# " << buffer << std::endl;
//根据用户输入,构建一个新的返回字符串
//echo_hello+="...";
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
}
else{
//TODO
}
}
return 0;
}