1. 网络通信的理解
主机A将自己的数据交给主机B,就需要给主机B发送消息,主机B未来要给主机A回消息
但实际上 主机A将自己的数据交给主机B 并不是最终目的
如:你在淘宝上买了一件衣服,卖家发货后,从广东省发货 到 你所在的地区 ,最终包裹成功到达你的手上,你还需要决定这个快递该怎么用
数据的传送不是目的,让两台主机通过数据进行通信来协同完成任务才是目的
如:唐僧说要去西天去取经,唐僧所对应的寺庙是A主机,西天的大雷音寺是B主机,唐僧并不是到大类饮食就完了,这只是他的手段, 他还需要面见如来,如来会提供给他经书的服务
数据发起时,从主机A的传输层开始,交给主机B的传输层 而数据是从主机A的应用层中的某种客户端传来的 而将数据交给主机B的传输层不是直接目的,要把数据再交给应用层 中的某种服务器
主机A对应的客户端一定要启动起来,所以其本质是 进程
因为主机B的某种服务器在以进程的方式运行,所以可以随时随地能够访问某种服务
网络通信的本质是 进程间的通信
通信的第一个阶段:先将数据通过操作系统,将数据发送到目标主机(手段) 通信的第二个阶段:在本主机将收到的数据,推送给自己上层的指定进程
第一个阶段 可以通过TCP/IP协议完成,因为IP可以表示互联网上唯一的一台主机
当主机B的传输层把数据交给应用层,应用层对应的进程非常多 所以为了标识自己主机上网络进程的唯一性,提出了 端口号 的概念
端口号是传输层协议的字段,是一个2个字节16位的整数,用来标识系统层面上进程的唯一性
所以 IP地址 端口号 可以表示 互联网中唯一的一个进程
通信时,是有两个进程进行通信,所以就有源IP 和源 端口号 以及 目标IP 和目标 端口号 源IP 和源 端口号表示 互联网中唯一的一个进程 目标IP 和目标 端口号也表示 互联网中唯一的一个进程
所以 网络通信的本质 是通过IP PORT号 构建唯一性,来进行网络进程间通信, 简称 套接字通信
2.进程PID可以取代端口号吗?
进程PID在系统层面上每个进程也是唯一的,也能表示该系统上进程的唯一性,所以用进程PID可以代替端口号的 但会存在一些问题 1.不是所有的进程都要进行网络通信,只有部分进程可能会网络通信,若用进程PID来作为网络标识该进程,就很难区分清楚那些是进行网络通信的,那些不是进行网络通信的
2. PID是操作系统进程管理的概念,网络模块也要包含进程管理的部分,要不然无法认识PID 就增加了系统当中进程管理和网络管理的耦合度
3. 认识TCP协议
TCP协议(Transmission Control Protocol) 传输控制协议 特点: 传输层协议 面向连接 在通信过程中,会自带可靠性 面向字节流 在进行发和收数据时,在TCP层没有报文的概念,收到一堆的数据,把这一堆的东西一次将给上层的应用层,也可一个字节一个字节交 字节数据如何解释TCP不关心,只关心要都多少,给你多少,最终解释信息由应用层自己解释,这种从称之为字节流
4. 认识 UDP协议
UDP协议(User Datagram Protocol)用户数据报协议 特点: 传输层协议 无连接 不可靠传输 面向数据报 如:收快递,收一个就是一个完整的快递,具体的快递不可能收半个或者一个半,若对方发了三次,你就必须收三次
5. socket编程接口
实验室做出来一套进程间通信的标准,既可在本地通信,又可以在网络跨主机通信的标准 即 socket标准 隶属于 posix标准
最常见的为 基于网络通信的套接字 sockaddr_in
预间套接字 (使用在两个进程间使用本地进程通信的) sockaddr_un
套接字的设计者为了能够让所有人以 一套接口的方式 既能本地通信 又能网络通信, 所以设计出一个公共的数据结构 叫做 struct sockaddr 若想进行网络通信 (struct sockaddr_in) 或者 进行 本地通信 (struct sockaddr_un) ,使用 sockaddr 进行强制转换即可
在结构最开始时,都要有16位的地址类型 AF_INET 与AF_UNIX 实际上都是宏,用整数来表示的 将地址进行比较判断, 若等于 AF_INET,就为网络通信,把 sockaddr强转为 sockaddr_in 若等于 AF_UNIX,就为本地通信,把 sockaddr强转为 sockaddr_un
udp_server.hpp的代码解析
通过网络协议栈的通信功能 ,来把数据交付给对方的应用层,来完成双方进程的通信
将客户端的数据交给 服务端 ,就需要给服务端发送消息,服务端再给客户端回消息
在 udp_server.hpp 中 使用namspace 将命名空间 命名为 ns_server 其中再定义一个类 udpserver
socket——创建 socket 文件描述符
输入 man socket
,创建套接字
第一个参数 domain ,用于区分 进行网络通信还是 本地通信 若想为网络通信,则使用 AF_INET 若想为本地通信,则使用 AF_UNIX
第二个参数 type, 套接字对应的服务类型
SOCK_STREAM 流式套接 SOCK_DGRAM 无连接不可靠的通信(用户数据报)
第三个参数 protocol ,表示想用那种协议,协议默认为0 若为 流式套接,则系统会认为是TCP协议 ,若为用户数据报,则系统会认为是UDP协议
套接字的返回值:若成功则返回文件描述符,若失败则返回 -1
Initserver——初始化
1.创建套接字接口,打开网络文件
使用socket套接字,创建出 网络通信、UDP协议 若套接字返回-1表示失败,则初始化也就失败,程序就没有必要在继续运行了,所以使用exit终止程序
若套接字创建成功,则返回文件描述符 文件描述符的前三个分别被 标准输入 标准输出 标准错误占用,所以此时的文件描述符应该打印出3
bind——绑定的使用
输入 man 2 bind
,查看绑定
给一个套接字绑定一个名字 第一个参数 sockfd 为 文件描述符 第二个参数 addr 为 通用结构体类型 第三个参数 addrlen 为 第二个参数的实际长度大小
bind返回值:若成功,则返回0,若失败,返回 -1
2.给服务器指明IP地址和端口号
想要使用struct sockaddr_in类型 需添加头文件
定义一个 struct sockaddr_in(网络通信) 类型的 变量 local
struct sockaddr_in的理解
将 struct sockaddr_in 转到定义 16位地址类型:将 sa_prefix替换成 sin_ ,sin## family 实际上为 sin_family 此时的 sin_port 对应 当前绑定的端口号 sin_addr对应的是IP地址 再次将 in_addr转到定义,IP地址就是一个32位的整数
bzero 清空
sin_zero 作为 该结构体的填充字段 结构体可能很大,用不完,则使用填充字段将其填充上即可
输入 man bzero
将有n个字节的缓冲区,全部写为0
代码实现
将local对应的family(16位地址类型) 设置为 网络通信
设置一个私有的端口号port_
在类外设置一个端口号,用于构造时,若没有端口号传入,则8082充当缺省值
若我给你发消息,未来也需要将消息发回来,所以就必须知道我的IP地址和端口号 即端口号 以报文的形式发送到网络中
类内定义的port_,被称为本地主机序列, 需要把这个port_从主机序列 转成网络序列
输入 man htons ,表示短整数的主机转网络序列
定义一个私有的变量 ip_ 由于我们设置的IP地址是字符串风格的,而系统中的IP地址是4字节风格的 所以就需要将字符串风格的转化为 4字节风格的
inet_addr ——字符串风格转化为4字节风格
输入 man inet_addr
作用为:将字符串风格的IP地址 转化为 4字节风格的IP地址,并 默认会把主机序列 转换为 网络序列
由于local实际上定义在用户层的栈上,并没有在内核
所以借助bind,将填充好的套接字字段和文件字段,进行绑定关联,这样的文件才是网络文件 由于local 是 struct sock_addr_in 类型 ,需要强转为 struct sockaddr 公共类型
服务器自己指定IP地址
此时运行 udp_server可执行程序,会发现套接字创建成功,但绑定会失败
云服务器 不需要bind IP地址,需要让服务器自己指定IP地址
所以在main函数中添加命令行参数 命令行参数 main函数的两个参数,char* argv[] 为指针数组 ,argv为一张表,包含一个个指针,指针指向字符串 int argc,argc为数组的元素个数
设计一个usage函数,用以表示出 出现问题的可执行程序的名字 proc
再次创建一个err.hpp,使用enum枚举,将USAGE_ERR设置成1 ,默认将SOCKET_ERR(套接字报错)设置为2, 将 BIND_ERR(绑定错误)设置为3
通过argv数组的第二个下标指明字符串风格的端口号,再通过atoi将字符串转化为整数 最终只传入 端口号即可
3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
使用 INADDDR_ANY , 让udpserver在启动的时候,bind本主机上的任意IP
将 INADDDR_ANY 转到定义,实际上为缺省的0值
start ——启动
服务器本质是一个死循环,永远不退出 如:半夜打开王者荣耀,依旧可以玩
1. 收到客户端发来的消息
recvfrom——获取用户数据报
输入 man recvfrom, 获取用户数据报
第一个参数 sockfd 为 套接字 第二个参数 buf 为 自己定义的缓冲区 第三个参数 len 为 缓冲区的长度 第四个参数 flags 为读取方式,默认设为0,以阻塞方式读取 剩余两个参数 src_addr 和 addrlen 为 输入 输出型 参数 使用recvfrom收到数据,最终还要把数据还回去,想要还回去就必须知道别人是谁 src_addr 为 作为一个结构体,内部记录客户端的IP地址和端口号 addrlen 为 输出时结构体的大小 返回值:若大于0,则读取成功
定义一个 struct sockaddr_in(网络通信) 类型的 变量 peer 使用 len 来表示 未来的结构体大小
若n大于0,则读取成功,将最后一个位置的下一个位置设为 若读取失败,则继续读取
peer下的IP地址为 4字节整数,需要将其转为字符串风格
inet_addr ——将4字节风格转为字符串风格
输入 man inet_addr
,将4字节IP转为字符串风格的IP
peer下的端口号为网络序列,想要获取客户端的端口号 clientport,需要使用 ntohs 将网络序列转为主机序列
2.将消息发给别人
sendto
输入 man sendto
第一个参数 sockfd 为 套接字 第二个参数 buf 为 自己定义的缓冲区 第三个参数 len 为 缓冲区的长度 第四个参数 flags 为读取方式,默认设为0,以阻塞方式读取 剩余两个参数 src_addr 和 addrlen 为 输入 输出型 参数 使用recvfrom收到数据,最终还要把数据还回去,想要还回去就必须知道别人是谁 src_addr 为 将以前收到的消息转会给客户端 addrlen 为 输出时结构体的大小 返回值:若大于0,则读取成功
udp_client.cc的代码解析
第一个参数 使用 AF_INET
,表示网络通信 第二个参数 使用SOCK_DRAM
,表示数据报 第三个参数 默认设为0
,由于上述为数据报,所以为UDP协议
客户端如何绑定?
客户端是需要绑定的 socket通信的本质 是 客户端的IP与端口号 与 服务器的IP与端口号 进行网络版本的进程间通信 但客户端是不需要自己绑定的,由操作系统自动进行绑定 如:电脑和手机充满大量客户端,这些客户端来自于不同的企业,每个客户端的端口号不可以是固定的 必须让操作系统随机去选择,本质是为了防止确定的客户端被别人去占用,减少客户端层面的冲突 所以客户端的端口号要让操作系统随机分配,防止客户端出现启动冲突
服务器为什么要自己绑定?
1.服务器的端口 是 众所周知并不能随意改变的 如:110是报警电话,不可能报警电话每天都变,否则会导致当真正想打电话时都不知道打那个
2.服务器都是一家公司的,所以端口号需要统一规范化 如:淘宝不会把自己的服务部署到知乎上
代码实现
进行while循环,向服务器发送消息
目前没有消息,所以让用户输入充当消息源 使用 sendto,将消息发送给服务端
作为客户端将消息发送给 服务器主机 想要运行 客户端 ,就需要服务器的IP 和端口号
借助命令行参数,通过用户的输入的第二个参数 作为服务器的IP 用户输入的第三个作为 服务器的端口号
虽然此时服务器的IP和端口号知道了,但是想要借助sendto,后两个参数是需要套接字结构体
新建一个结构体server,内部包含服务器的IP和端口号 使用 htons ,将主机序列转为网络序列 使用inet_addr,将字符串转化为 4字节
此时 sendto的后两个参数 添加 创建的结构体 sever ,来完成发送服务器的任务 由于server 的类型 是 struct sockaddr_in ,而参数的类型为 公共结构体类型 struct sockaddr ,所以需要强转
使用 revfrom ,获取用户数据报 收到来自服务器转回来的消息 ,所以 定义一个 temp结构体,用于接收
在首次系统调用发送数据的时候,操作系统在底层随机选择客户端的端口号 加上自己的IP 先构建bind,再构建发送的数据报文
完整代码
err.hpp (枚举错误码)
代码语言:javascript复制#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR
};
makefile
代码语言:javascript复制.PHONY:all
all: udp_client udp_server
udp_client:udp_client.cc
g -o $@ $^ -std=c 11
udp_server:udp_server.cc
g -o $@ $^ -std=c 11
.PHONY:clean
clean:
rm -f udp_clinet udp_server
udp_client.cc(客户端的实现,无封装)
代码语言:javascript复制#include"udp_client.hpp"
#include"err.hpp"
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static void usage(std::string proc)
{
std::cout<<"usage:nt"<<proc<<"serverip serverportn"<< std::endl;
}
// ./udp_client serverip sevrerport
int main(int argc ,char* argv[])//命令行参数 传入的是 客户端的运行 服务器的IP和端口号
{
if(argc!=3)
{
std::cout<<" "<<std::endl;
exit( USAGE_ERR);//终止程序
}
std::string serverip = argv[1];//服务器的IP
uint16_t serverport =atoi(argv[2]);//服务器的端口号
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)//创建套接字失败
{
std::cout<<"create socket error"<<std::endl;
exit( SOCKET_ERR);
}
//明确server是谁
struct sockaddr_in server;//设置网络通信的结构体
memset(&server,0,sizeof(server)); //将结构体清空
server.sin_family=AF_INET;
server.sin_port=htons(serverport);//端口号
server.sin_addr.s_addr=inet_addr(serverip.c_str());//IP地址
while(true)
{
//用户输入
std::string message;
std::cout<< "please enter# ";
std::cin>> message;
//发送消息
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
//接收消息
char buffer[1024];
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(n>0)
{
buffer[n]=0;
//收到回显消息
std::cout<<"server echo"<<buffer<<std::endl;
}
}
return 0;
}
udp_clinet.hpp
代码语言:javascript复制#pragma once
#include<iostream>
using namespace std;
udp_server.cc (有封装)
代码语言:javascript复制#include"udp_server.hpp"
#include"err.hpp"
#include<memory>
#include<string>
using namespace ns_server;
using namespace std;
static void usage(string proc)
{
std::cout<<"usage:nt"<<proc<<"protn"<< std::endl;
}
//udp_server port
int main(int argc,char*argv[])//命令行参数
{
if(argc!=2)//若命令行参数个数不为2,则当前会报错
{
usage(argv[0]);
exit(USAGE_ERR);//终止程序
}
//端口号
uint16_t port=atoi(argv[1]);//atoi可将字符串转化为整数
//只需传入由用户指明的端口号
unique_ptr<UdpServer> usvr(new UdpServer (port));
usvr->Initserver();//服务器的初始化
usvr->Start();//启动服务器
return 0;
}
udp_server.hpp(服务器的实现)
代码语言:javascript复制#pragma once
#include<iostream>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<strings.h>
#include<functional>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<sys/socket.h>
#include"err.hpp"
namespace ns_server
{
const static uint16_t default_port=8082;//设置端口号为8082
class UdpServer
{
public:
UdpServer(uint16_t port=default_port)//构造
:port_(port)
{}
void Initserver()//初始化
{
//1.创建套接字接口,打开网络文件
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)//创建失败
{
//打印错误信息
std::cout<<" create socket error: "<<strerror(errno)<<std::endl;
exit(SOCKET_ERR);//终止程序
}
std::cout<<"create socket success:"<<sock_<<std::endl;//3
//2.给服务器指明IP地址和端口号
struct sockaddr_in local;
bzero(&local,sizeof(local));//全部置为0
local.sin_family=AF_INET;//将16位地址类型 置为 网络通信
local.sin_port= htons(port_); //主机转网络的端口号
//1.需要将字符串风格转化为 4字节
//2.需要 将主机序列转换为 网络序列
local.sin_addr.s_addr= INADDR_ANY ; //bind本机上的任意IP
//bind 绑定
int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));
if(n<0)//绑定失败
{
std::cout<<" bind socket error: "<<strerror(errno)<<std::endl;
exit(BIND_ERR);
}
std::cout<<"bind socket success:"<<sock_<<std::endl;//3
}
void Start()//启动
{
char buffer[1024];//用于保护用户数据
//设置一个死循环
while(true)
{
//1.收到客户端发来的消息
struct sockaddr_in peer;
socklen_t len=sizeof(peer);//传入的缓冲区大小
int n=recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
buffer[n]='