1.网络编程核心概念:
协议:为进行数据通信而预定义的数据规则
地址:网络通信中的用于标识设备的整数值
端口号:
设备为收发数据而指定的数值,用于标识具体连接
可理解为:设备中用于网络通信的数据通道
服务端:等待连接的设备
客户端:发起连接的设备
网址就是IP地址吗?URL是什么?,域名又是什么?
网址不是IP地址,是网络信息资源的地址(如:具体网页的地址),即:URL
域名是IP地址的别名,多个域名可指向同一个IP地址
协议一定是看不懂的二进制数据吗?
协议是一种约定,协议可以基于文本定义,也可以基于二进制定义
网络字节序:网络字节顺序采用大端模式,所以:在小端系统中需要做字节序转换
#include#include int socket(int domain, int type, int protocal); //创建套接字,为网络连接做准备 int connect(int sock, struct sockaddr* addr, socklen_t len); //连接指定地址的远程设备 ssize_t send(int fd, const void* buf, size_t n, int flags); //发送数据到远程设备 ssize_t recv(int fd, void* buf, size_t n, int flags); //接受远程设备发回的数据 int close(int fd); //关闭连接,销毁套接字
2.服务端编程
客户端/服务端 编程模式
服务端长期暴露与网络,并等到客户端连接
客户端发起连接动作,并等待服务端回应
特点:
服务端无法主动连接客户端
客户端只能按照预定义的方式连接服务端
服务端核心工作;绑定&监听&接受
绑定:
int bind(int sock, struct sockaddr* addr, socklen_t addrlen);
监听:
int listen(int sock, int backlog);
接受:
int accept(int sock, struct sockaddr* addr, socklen_t* addelen)
深度剖析服务端:
服务端socket只用于接受连接,不进行实际通信
当接受到连接时,accept()函数返回与客户端通信的socket
服务端socket产生用于通信的客户端socket
socket究竟是什么玩意?如何理解?
socke()是一个多功能函数,返回值是用于通信的资源标识符,socket()可提供不同类型的通信功能(本地进程间通信)
客户端/服务端编程的核心模式:
服务端长时间运行(死循环)接受客户端请求
客户端连接后向服务端发送请求(协议数据)
深入浅出IP地址:
网络编程接口中一些参数的意义是什么?
sock = socket(PF_INET, SOCK_STREAM, 0);
socket参数详解:
domain:套接字中使用的协议族信息 (PF_INET:IPv4互联网协议族,PF_INET6:IPv6互联网协议族,PF_LOCAL:本地通信的协议族,PF_PACKET:底层数据收发协议族,PF_IPX:Novell专用协议(互联网分组交换协议))
type:套接字数据传输类型信息,用于指定协议类型 (SOCK_STREAM:流式数据(TCP),SOCK_UGRAM:报文式数据(UDP))
protocol:设备间通信使用的协议信息 ,用于指定协议族中符合类型的具体协议 (domain和type几乎可以唯一确定一种协议,因此,这个参数通常为0)
端口号和IP地址:
端口号是一个2字节数据
0-1023作为特定端口被预定义(分配给特定应用程序)
IP地址是一个4字节地址族(可分为5类地址)
深入解析IP地址:
IP地址分为网络标识和主机标识两部分
网络标识:标识网络主机(设备)所在的网络
主机标识:标识网络主机具体地址
一个IP地址就4个字节,如何区分网络标识和主机标识呢?
IP地址与子网掩码配合使用区分网络标识和主机标识
子网掩码的表现形式也是一个4字节的整形数
子网掩码用于从IP地址中提取网络标识
深入理解子网掩码:
设:子网掩码为M.N.P.Q,则子网可用IP地址 n = (256 - M)(256-N)(256-p)*(256-Q)
IP地址211.99.34.33,掩码255.255.255.248
可知211.99.34.33所在子网有8个IP地址,且8=2的3次方,所以Y = 32 - 3 = 29
表示为211.99.34.33/29(简介表示法)
特殊的地址 0.0.0.0/0 - 保留,常用于代表"缺省网络" 127.0.0.08 -回环地址,常用于本地软件回送测试 255.255.255.255/32 - 广播地址 私有地址:不在公网使用,只在内网使用 10.0.0.0 - 10.255.255.255/8 172.16.0.0 - 171.31.255.255/16 192.168.0.0 - 192.168.255.255/24
IP地址相关函数:
#include in_addr_t inet_addr(const char* strptr); //将IP字符串转换为符合网络字节序的整数 int inet_aton(const char* cp, struct in_addr* inp); //将IP字符串转换为符合网络字节序的整数,成功返回1,失败返回0 char* inet_ntoa(struct in_addr in); //将符合网络字节序的整数地址转换为字符串形式
尝试select多路复用:
如何增强服务端能力,同时支持多个客户端?
linux中的文件是什么?
狭义:文件系统中物理意义上的文件
广义:
设备,管道,内存。。。
linux管理的一切对象
文件描述符:
文件描述符是一个非负整数值,本质是一个句柄
一切对用户透明的资源表示都可以看做句柄
用户使用文件描述符与内核交互
内核通过文件描述符操作对应资源的数据结构
事件相关函数的分类:
阻塞式函数:
函数调用后需要等待某个事件发生后才会返回
非阻塞式函数:
函数调用后能够及时返回(仅标记等待的事件)
事件发生后以回调方式传递
神奇的select()函数:
select()用于监视指定的文件描述符是否产生事件
可通过轮询的方式检测目标事件(事件产生则标记发生变化)
根据事件类型做出具体处理(如:读取数据)
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptest, const struct timeval* timeout);
select()相关数据类型及操作:
FD_ZERO(fd_set* fdset); //将fd_set变量的所有位设置为0 FD_SET(int fd, fd_set* fdset); //在fd_set中指定需要监听的fd FD_CLR(int fd, fd_set* fdset); //在fd_set中剔除fd1,不在监听 FD_ISSET(int fd, fd_set* fdset); //在fd_set查看是否包含fd
基于多路复用的服务端
问题:使用select()函数可以扩展服务端功能吗?如果可以 ,具体怎么实现?
服务端瓶颈分析:
client = accept(server, (struct sockaddr*)&caddr, &asize); //阻塞,等待客户端连接
r = recv(client, buf, sizeof(buf), 0); //阻塞,等待客户端数据
服务器大多时候处于等待状态,无法发挥主机的最大性能
解决方案:阻塞变轮询
通过select()函数首先监听服务端server_fd,目标事件为“连接”(读)
当事件发生(客户端连接)则调用accept()接受连接
将client_fd加入监听范围,目标事件为“数据接受”(读)
循环查看各个被监听的文件描述符是否有事件发生
实现逻辑:
while(1)
{
rset = reads;
num = select(max+1, &rset, 0, 0, &timeout);
if(num > 0)
{
int i = 0;
for(i = 1;i <= max; i++)
{
if(FD_ISSET(i, &rset))
{
if(i == server)
{
//accept and client to fd_set
}
else
{
//read data from client by i (fd)
}
}
}
}
}
实现关键:
动态调整需要监视的文件描述符
当接受到客户端连接时,将客户端文件描述符加入到监听变量(fd_set)中
当发现客户端断开时,在监听变量(fd_set)中剔除客户端文件描述符
if(client > -1)
{
FD_set(client, &reads);
max = (client > max) ? client : max;
printf("client: %dn", client);
}
if(r == -1) FD_CLR(i, &reads);
实现关键:
动态调整需要监视的文件描述符数量
保证每个需要监视的文件描述符能够被轮询
max = (client > max) ? client : max;
TCP与UDP:
TCP/IP分层结构:
应用层:各个应用程序可以定义(使用)各种各样的协议
传输层:确保发出的数据能够达到目标主机,完成数据传输
网络层:填写数据包地址,选择数据传递路径
数据链路层:融合不同连接方式的链路,屏蔽网络差异
物理层:具体连接方式:有线,无线,光纤
TCP / IP工作方式:
TCP/IP层次结构的特点:
上层依赖邻接下层的能力,下层只为直接邻接上层服务
上层不知道下层的工作机制,下层不管上层传输的数据内容
不做跨层服务,层次结构中的角色缺一不可
深入理解网络层(IP层):
IP地址:IP地址属于网络层地址,用于标识网络上的主机
路由控制:控制数据如何到达目标主机(如:需要经过那些路由器转发)
无连接:数据包根据IP地址在网络上传递(无需与目标实现简历连接)
MAC地址:数据链路层所使用的硬件地址。MAC地址与网络无关,出厂时写入到网络设备中。当主机从网络撒花姑娘每收到一个数据帧时,首先检查数据帧中的MAC地址。如果是发生本主机的数据帧则收下,之后进行其他的处理;否则将此帧丢弃,不再进行其他的处理。
IP地址和MAC地址:
IP地址是动态的,不特定于某个具体的硬件(MAC地址隶属于具体硬件)
IP地址是网络层使用的地址(用于夸网络投递数据包)
MAC地址是数据链接层使用的地址(用于确定目标网络中接受数据的主机)
路由器中记录了本网络中主机IP地址与MAC地址的映射关系
IP路由控制:
·为了将数据发给目标主机,所有主机都维护着一张路由表
路由表记录了IP数据包下一步应该发给哪个路由器
IP数据转发:
·IP数据包转发用的是“尽力服务”策略
"尽力服务"指会努力,但不保证结构
转发时会通知附加信息检查数据合法性,但出现异常不会进行长重发
以包为单位进行转发,不保证到达(发出之后,石沉网海)
问题:TCP/IP网络层次结构能否能提供可靠数据传输
传输控制协议:
TCP在协议实现上提供可靠数据传输
TCP不存在“数据包”的概念,实现了流失传输(数据如流水,无头无尾)
TCP内部有服务状态,能够精确知道数据是否已经发送成功,是否被接受。。。
TCP在行为上可进行阻塞控制(网络环境变差时,能够调整数据发送速度)
TCP连接建立:
TCP连接建立:
TCP的天生缺陷(DDos攻击):
·TCP连接断开:
UDP的特点:完全继承网络层工作方式
无需连接,直接指定IP地址和端口即可发送数据
监听固定端口,只要有数据,统统接受
不管网络情况,只要是数据统统可发送
不关心数据是否到达对端
UDP使用场合:
对数据不敏感,需要实时性的场合(如:直播,实时游戏)
网络环境比较好的场合(如:物联网结局)
需要深度定制协议的场合(如:不丢包的UDP协议)
应用层协议设计与实现:
小知识:
发送缓冲区:
数据先进入发送缓冲区,之后由操作系统送往远端主机
接受缓冲区:
远端数据被操作系统接受后放入缓冲区
之后应用程序从接受缓冲区读取数据
TCP应用编程中的“问题”:
数据接受端无法知道数据的发送方式!!
网络编程中的期望:
每发送一条完整的消息,每次接受一条完整的消息
即使接受缓冲区中有多条消息,也不会出现消息粘连
消息中涵盖了数据类型和数据长度等信息
应用层协议设计:
什么是协议?
协议是遵循通信双方为数据交换而建立的规则、标准或约定的集合
协议对数据传输的作用
通信双方根据协议能够正确收发数据
通信双方根据协议能够解释数据的意义
协议设计示例:
目标:设计可用于数据传输的协议
完整消息包含
数据头:数据类型(即:数据区用途,固定长度)
数据长度:数据区长度(固定长度)
数据区:字节数据(变长区域)
协议设计示例:
因此:消息至少12个字节(消息头 + 数据长度)
通过计算消息的总长度,能够避开数据粘连性的问题
协议设计示例:
typedef struct
{
unsigned short type;
unsigned short cmd;
unsigned short index;
unsigned short total;
unsigned int length;
unsigned char payload[]; //柔性数组
}Message;
应用层协议解析模块(上):
问题:如何在代码层面封装协议细节?如何将接受缓冲区中的数据解析成为Message
深度思考:
数据是否能够解析成为Message?
数据量足够
如果数据量足够,是否能够解析不知一个Message?
如何处理剩余数据(属于下一个Message)?
数据量不足
是否达到协议最小长度(12字节)?
如何处理数据量超过最小长度,但不足以创建一个Message的情况?
初步的解决方案
定义一个模块用于从字节流解析Message
可从指定内存或从指定文件描述符读取并解析
当至少存在12个字节时开始解析
1.首选解析协议中的头信息和数据区长度
2.根据数据区长度继续从字节流读取数据
3.当协议数据解析完成时,创建Message并返回,否则,返回NULL
协议解析模块的初步设计:
解析器数据结构:
typedef struct msg_parser
{
Message cache; //缓存已解析的消息头
int header; //标识消息头是否解析成功
int need; //标识还需要多少字节才能完成解析
Message* msg; //解析中的协议信息(半成品)
}MsgParser;
从内存中解析协议数据
条件:内存长度至少连续12个字节
memcpy(&p->cache, mem, p->need); //从网络字节序转化为本机字节序 p->cache.type = ntohs(p->cache.type); p->cache.cmd = ntohs(p->cache.cmd); p->cache.index = ntohs(p->cache.index); p->cache.total = ntohs(p->cache.total); p->cache.length = ntohs(p->cache.length); mem += p->need; length -= p->need; p->header = 1; p->need = p->cache.length;
从内存中读取payload中的数据(可多次读取)
if(!p->msg)
{
//成功解析消息头之后,创建Message
p->msg = malloc(sizeof(p->cache) + p->need);
if(p->msg)
{
*p->msg = p->cache;
}
}
if(p->msg)
{
unsigned int len = (p->need < length) ? p->need : length;
unsigned int offset = p->msg->length - p->need;
memcpy(p->msg->payload + offset, mem, len);
p->need -= len;
}



