文章目录
- 1. 网络通信的理解
- 2.进程PID可以取代端口号吗?
- 3. 认识TCP协议
- 4. 认识 UDP协议
- 5. socket编程接口
- udp_server.hpp的代码解析
- socket——创建 socket 文件描述符
- Initserver——初始化
- 1.创建套接字接口,打开网络文件
- bind——绑定的使用
- 2.给服务器指明IP地址和端口号
- struct sockaddr_in的理解
- bzero 清空
- 代码实现
- inet_addr ——字符串风格转化为4字节风格
- 服务器自己指定IP地址
- 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
- start ——启动
- 1. 收到客户端发来的消息
- recvfrom——获取用户数据报
- inet_addr ——将4字节风格转为字符串风格
- 2.将消息发给别人
- sendto
- udp_client.cc的代码解析
- 客户端如何绑定?
- 服务器为什么要自己绑定?
- 代码实现
- 完整代码
- err.hpp (枚举错误码)
- makefile
- udp_client.cc(客户端的实现,无封装)
- udp_clinet.hpp
- udp_server.cc (有封装)
- udp_server.hpp(服务器的实现)
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,则读取成功,将最后一个位置的下一个位置设为\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 (枚举错误码)
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR
};
makefile
.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(客户端的实现,无封装)
#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:\n\t"<<proc<<"serverip serverport\n"<< 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
#pragma once
#include<iostream>
using namespace std;
udp_server.cc (有封装)
#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:\n\t"<<proc<<"prot\n"<< 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(服务器的实现)
#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]='\0';
}
else
{
//读取失败,则继续读取
continue;
}
//提取客户端信息
//4字节IP转为 字符串IP
std::string clientip =inet_ntoa(peer.sin_addr);//客户端IP
//将网络序列转换为主机序列
uint16_t clientport =ntohs(peer.sin_port);//客户端 端口号
std::cout<<clientip<<"-"<<clientport<<"-"<<"get message# "<<buffer<<std::endl;
//2.将消息发给别人
sendto(sock_,buffer,strlen(buffer),0,(struct sockaddr*)&peer,sizeof(peer));
}
}
~UdpServer()//析构
{}
private:
int sock_; //文件描述符
uint16_t port_;//端口号
};
}