局域网广域网
局域网(LAN)
局域网的缩写是LAN,是一个本地网络,只能实现小范围短距离的网络通信。
广域网(WAN)
广域网的缩写是WAN,是相对于局域网来说的,局域网传输距离比较近,只能是一个小范围的。如果需要长距离传输,局域网是无法架设的
注释:某些教材也将“城域网(MAN)”划入网络分类,本质是介于局域网与广域网之间的产物,界限划分并不明确,如果一定要有区别的话,最大的特点就是三者在覆盖范围上有区别
网络设备
调制解调器 交换机 路由器 集线器
IP地址
概念
IP地址是Internet中主机的标识
Internet中的主机要与别的机器通信必须具有一个IP地址
IP地址为32位(IPV4)或者128位(IPV6)
表示形式:常用点分十进制
网络号/主机号
IP地址=网络号+主机号
网络号:表示是否在一个网段内(局域网)
主机号:表示在本网段内的ID,同一局域网的主机号不能重复
地址划分
主机号的第一个和最后一个都不能被使用,第一个作为网段号,最后一个作为广播地址
IP地址分为A类,B类,C类,D类,E类
两个特殊的IP地址
0.0.0.0:在服务器中,0.0.0.0指的是本机上所有的IPV4地址,如果一个主机有两个IP地址,,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过这两个IP地址都能访问该服务。INADDR_ANY
127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back,通常作为测试使用
子网掩码
子网掩码:是一个32位的整数,作用是将某一个IP划分为网络地址和主机地址
子网掩码长度是和IP地址长度完全一样 网络号全为1,主机号全为0 子网掩码一旦确定,主机号和网络号的位数就确定了,那么网络号是多少就确定了,同时,此局域网主机号的最大承载量也就确定了
公式:网络号=IP&MASK
网络模型
网络体系结构
常见的两种网络体系结构:TCP/IP与OSI
TCP/IP常见协议
网络接口和物理层:
ppp:拨号协议(老式电话线上网方式)
ARP:地址解析协议 IP-->MAC
RARP:反向地址转换协议 MAC-->IP
网络层:
IP(IPV4/IPV6):网间互连的协议
ICMP:网络控制管理协议,ping 命令使用
IGMP:网络分组管理协议,广播和组播使用
传输层:
TCP:传输控制协议
UDP:用户数据报协议
应用层:
SSH:远程登录会话安全协议
telnet:远程登录协议
FTP:文件传输协议
HTTP:超文本传输协议
DNS:地址解析协议
SMTP/POP3:邮件传输协议
TCP和UDP
TCP
TCP(传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信
适用场景:
适合于对传输质量要求较高的通信
在需要可靠数据传输的场合,通常使用 TCP 协议
UDP
UDP(用户数据报协议):是一种面向无连接的传输层协议,提供的服务是不可靠的,在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输
适用场景:
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
适合于广播/组播式通信中。
socket类型
流式套接字(SOCK_STREAM)TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGREAM)UDP 提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW) 可以对较低层次协议如IP、ICMP直接访问。
端口号
为了区分一台主机接收到的数据报应该转交给哪个进程来进行处理,使用端口号来区分
TCP端口号与UDP端口号独立
端口号一般由IANA管理 端口用两个字节来标识:0~65535
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)
1024~5000 操作系统会动态使用
5000~10000:编程中可用的
字节序
小端序:低字节存储在低地址
大端序:高序字节存储在低地址
注意:网络中传输的数据必须使用网络字节序,即大端字节序
//检查主机的字节序
int checkCpu(){
union w{
int a;
char b;
}
c.a=1;
return (c.b==1);
}
大小端序转换
//主机字节序到网络字节序
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
//网络字节序到主机字节序
u_long ntohl(ul_ong hostlong);
u_short ntohs(u_short hostshort);
IP地址转换
typedef unint32_t in_addr_t;
struct in_addr{
in_addr_t s_addr;
};
in_addr_t inet_addr(const char *cp);//从人看的ip地址转为机器使用的32位无符号整数
char *inet_ntoa(struct in_addr in);//从机器到人
//参考代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main(int argv,const char *argv[]){
//从人看的IP转机器
in_addr_t addr;
addr=inet_addr("192.168.3.1");
printf("addr=0x%x\n",addr);
//从机器转回IP
struct in_addr ipaddr;
ipaddrs_addr=addr;
printf("ip=%s\n",inet_ntoa(ipaddr));
}
TCP编程
流程
服务器:
1.创建流式套接字(socket())------------------------> 有手机
2.指定本地的网络信息(struct sockaddr_in)----------> 有号码
3.绑定套接字(bind())------------------------------>绑定手机
4.监听套接字(listen())---------------------------->待机
5.链接客户端的请求(accept())---------------------->接电话
6.接收/发送数据(recv()/send())-------------------->通话
7.关闭套接字(close())----------------------------->挂机
客户端:
1.创建流式套接字(socket())----------------------->有手机
2.指定服务器的网络信息(struct sockaddr_in)------->有对方号码
3.请求链接服务器(connect())---------------------->打电话
4.发送/接收数据(send()/recv())------------------->通话
5.关闭套接字(close())--------------------------- >挂机
API接口
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
//功能:创建套接字
/*
参数:
domain:协议簇
AF_UNIX,AF_LOCAL:本地通信
AF_INET: IPV4
AF_INET6:IPV6
type:套接字类型
SOCK_STREAM:流式套接字->唯一对应TCP
SOCK_DGREAM:数据报套接字->唯一对应UDP
SOCK_RAW:原始套接字
protocol:
domain和type已经确定协议和类型,因此填0即可
返回值:
新的套接字描述符(供后面接口使用)
-1:出错
*/
int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//功能:绑定套接字
/*
参数:
sockfd:套接字描述符
addr:网络信息结构体的指针
addrlen:网络信息结构体的大小
返回值:
成功:0
失败:-1
*/
//通用地址结构
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};
//IPV4协议地址结构
struct sockaddr_in{
sa_family_t sin_family;//AF_INET
in_port_t sin_port;//端口号
struct in_addr sin_addr;//ip地址结构体
};
//ip地址结构体
struct in_addr{
uint32_t s_addr;//ip地址主机号
};
/*
特殊的IP地址:
INADDR_ANY:标识监听本机的所有地址 0.0.0.0
127.0.0.1:本机回环地址
*/
int listen(int sockfd,int backlog);
//功能:监听套接字(将主动变为被动)
/*
参数:
sockfd:套接字描述符
backlog:监听队列的最大长度
返回值:
成功:0
失败:-1
*/
int accept(int sockfd,struct sockaddr *addr,socklen *addrlen);
//功能:接收客户端的请求,阻塞接口
/*
参数:
sockfd:监听套接字
addr:客户端网络信息结构体的指针
addrlen:客户端网络信息结构体的大小指针
返回值:
成功:已经连接的新客户端的文件描述符
失败:-1
*/
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
//功能:发送数据
/*
参数:
sockfd:套接字描述符
buf:发送缓冲区的首地址
len:发送缓冲区的大小
flags:0//阻塞发送
返回值:
成功:成功发送的字节个数
失败:-1
*/
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
//功能:接收数据
/*
参数:
sockfd:套接字描述符
buf:接收缓冲区的首地址
len:接收缓冲区的大小
flags:0//阻塞接收
返回值:
成功:成功接收的字节个数
失败:-1
*/
int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
//功能:请求连接服务器
/*
参数:
sockfd:套接字描述符
addr:服务器网络信息结构体指针
addrlen:网络信息结构体的大小
返回值:
成功:0
失败:-1
*/
TCP与UDP的异同
TCP和UDP同属于传输层协议
TCP是面向连接的,可靠的,流式套接字通信
UDP是面向无连接的,不可靠的,数据报套接字通信
UDP编程
流程
server:
1. 创建数据报套接字(socket(,SOCK_DGRAM,))--------------------->有手机
2.绑定网络信息(bind())----------------------------------->绑定号码(发短信知道发给谁)
3.接收信息(recvfrom())--------------------------------------->接收短信
4.关闭套接字(close())---------------------------------------->接收完毕
client:
1.创建数据报套接字(socket())----------------------------------->有手机
2.指定服务器的网络信息------------------------------------------->有对方号码
3.发送信息(sendto())------------------------------------------>发送短信
4.关闭套接字(close())----------------------------------------->发送完毕
API接口
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
//功能:接受数据
/*
参数:
sockfd:套接字描述符
buf:接收缓冲区的首地址
len:接收缓冲区的大小
flags:0
src_addr:发送端的网络信息结构体的指针
addrlen:发送端的网络信息结构体大小的指针
返回值:
成功:接收的字节个数
失败:-1
0:对端退出
*/
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
//功能:发送数据
/*
参数:
sockfd:套接字描述符
buf:发送缓冲区的首地址
len:发送缓冲区的大小
flags:0
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小
返回值:
成功:发送的字节个数
失败:-1
*/
Linux下的IO模式及其特点
阻塞式IO
特点:最简单,最常用,效率低
阻塞I/O模式是最普遍使用的I/O模式,大部分使用的都是阻塞式的I/O 缺省情况下,套接字建立之后所处的模式便是I/O模式 会发生阻塞的函数示例:
读操作:
read、recv、recvfrom
读阻塞->当被读取缓冲区包含数据时,解除读阻塞
写操作:
write、send
写阻塞->当写入数据超出缓冲区大小时会发生写阻塞,当缓冲区满足一定条件时,写阻塞解除
注意: sendto没有写阻塞,原因是udp本身是面向无连接的,因此udp没有发送缓冲区,因此sendto不是阻塞函数 UDP不用等待确认操作,没有实际的发送缓冲区因此,写操作不会阻塞
其他操作:
accept、connect
相关函数已经在之前赘述,这里就不再重复叙述了
非阻塞式IO
特点:可以处理多路IO;但是需要进行轮询操作,因此浪费CPU资源
非阻塞式IO并不会等待阻塞程序执行完成,而是会立刻抛出一个错误
其次,一个应用程序使用了非阻塞模式的套接字后,需要使用一个循环来不停地测试文件描述符是否可读
因此应用程序需要不断访问内核来检查I/O操作是否就绪,这是一个很浪费CPU资源的过程
一般不会使用非阻塞式I/O进行操作
非阻塞式IO的工作模式
如何设置非阻塞模式
方式一:通过函数参数进行设置
//recv函数的最后一个参数为0时,证明此函数为阻塞式IO,当参数为MSG_DONTWAIT表示非阻塞
//示例:
recvbyte=recv(acceptfd,buf,sizeof(buf),MSG_DONTWAIT);
//非阻塞的含义表示需要循环检测数据是否被传递,需要进行轮询操作,机器浪费CPU资源
方式二:通过fcntl函数进行设置
int fcntl(int fd,int cmd,.../*arg*/);
//功能:设置文件描述符的属性
/*
参数:
fd:文件描述符
cmd:功能选择
状态属性:
F_GETFL->获取文件描述符原来的属性
F_SETFL->设置文件描述符属性
arg:根据cmd决定是否填充值 int
返回值:
成功:F_GETFL:返回值的文件描述符号属性的值int
F_SETFL 0
失败:-1
*/
简单案例
#include <stdio.h>
#include <nistd.h>
#include <fcntl.h>
int main(int argc,const char * argv[]){
int flags=fcntl(0,F_GETFL);
flags|=O_NONBLOCK;
fcntl(0,F_SETFL,flags);
}
信号驱动IO
特点:异步通知模式,但是需要底层驱动的支持,部分操作系统可能不支持
通过信号的方式,当内核检测到设备的数据后,主动给应用程序发送SIGIO信号
应用程序收到信号后进行异步处理
应用程序需要把自己的进程号告诉给内核,并打开异步通知机制
//这是一个模板
//将APP进程号告诉给驱动程序
fcntl(fd,F_SETOWN,getpid());//设置程序具有异步的权限
//开通异步通知
int flag=fcntl(fd,F_GETFL);
flag|=O_ASYNC;//这里的标识不唯一,可以将O_ASYNC替换为FASYNC标识
fcntl(fd,F_SETFL,flag);
signal(SIGIO,handler);//当io操作产生时便会触发,这里的handler是一个函数
IO多路复用
IO多路复用机制
应用程序中同时处理多路输入输出流,若采用阻塞模式,将达不到预期的目的;若采用非阻塞模式对多个输入进行轮询,但又太浪费CPU时间;若设置多个线程/进程,分别处理一条数据通路,将产生进程/线程的同步通信问题,使问题变得复杂
比较好的方法是使用I/O多路复用技术,IO复用技术的基本思想:
先构造一张有关描述符的表,然后调用一个函数
当这些文件描述符中的一个或者多个已经准备好IO时,函数才返回 函数返回时告诉进程那个描述符已经就绪,可以进行IO操作
基本流程
先构造一张有关文件描述符的表(集合、数组)
将你关心的文件描述符加入到这个表中
然后调用一个函数 select/poll
当这些文件描述符中的一个或多个已准备好进行IO操作时,该函数返回
判断哪一个或者哪些文件描述符产生了事件(IO操作)
做对应的逻辑处理
实现IO多路复用的方式及其函数功能(函数接口)
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
//功能:select 用于检测是哪个或者哪些文件描述符产生的事件
/*
参数:
nfds:检测的最大文件描述符个数
readfds:读事件集合//一般操作都是读操作
writefds:写事件集合//NULL标识不关心
exceptfds:异常事件集合
timeout:超时检测1 如果不做超时检测:传递NULL
返回值:
<0:表示出错
>0:表示有事件产生
如果设置了超时检测时间&tv
<0:出错
>0:有事件产生
=0:超时时间已到
struct timeval{
long tv_sec;//seconds
long tv_usec;//microseconds
};
*/
void FD_CLR(int fd,fd_set *set);//将fd从表中清除
int FD_ISSET(int fd,fd_set *set);//判定fd是否在表中
void FD_SET(int fd, fd_set *set);//将fd添加到表中
void FD_ZERO(fd_set *set);//清空表
/*
注:select一旦返回会将没有产生事件的文件描述符从表中清空。下次检测需要重新添加。表中的下标是文件描述符的值,添加的时候是根据对应位置添加的,检测个数是添加进表中最大的文件描述符+1
*/
总结select实现IO多路复用的特点
1.一个进程最多只能监听1024个文件描述符(千级别)//可以修改,但是不推荐
2.select每次被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源)
3.select每次都会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3~4G是内核态,拷贝是非常耗时的)
代码步骤示例
fd_set readfd,tempfd;
FD_ZERO(&readfd);
FD_SET(0&readfd);
FD_SET(sockfd,&readfd);
int max=sockfd;
while(1){
tempfd=readfd;
select(max+1,&tempfd,NULL,NULL,NULL);
if(FD_ISSET(0,&tempfd)){
}
if(FD_ISSET(sockfd,&tempfd)){
}
}
poll实现IO多路复用
函数接口
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//功能:监视并等待多个文件描述符的属性变化
/*
参数:
fds:关心的文件描述符结构体
nfds:文件描述符个数
timeout:超时检测 毫秒级 -1阻塞
返回值:
如果不做超时检测:-1
<0:出错
>0:表示有事件产生
如果设置了超时检测
<0:出错
>0:表示有事件产生
=0:表示时间已到
*/
//pollfd结构体
struct pollfd{
int fd;//检测的文件描述符
short events;//检测事件
short revents;//调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员,只需要判断这个成员的值就可以确定是否产生了事件
}
//事件:
POLLIN//读事件
POLLOUT//写事件
POLLERR//异常事件
//注意:一般监听消息都是读消息,终端输入也属于读消息,accept也属于读取与客户端之间的连接
poll实现IO多路复用的特点:
1.优化文件描述符个数的限制(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1个,由程序员自己来确定)
2.poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
3.poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
代码步骤示例
//创建表
struct pollfd fds[N]={};
//将关心的文件描述符添加到表中
fds[0].fd=0
fds[0].events=POLLIN;//读事件
fds[1].fd=1;
fds[1].events=POLLIN;
int num=2;
int ret;
while(1){
//调用poll函数循环检测是否有事件产生
ret=poll(fds,num,-1);//阻塞等待事件产生返回
if(fds[0].revents==POLLIN){
}
if(fds[1].revents==POLLIN){
}
}
epoll实现IO多路复用
epoll实现机制
epoll的提出:它所支持的文件描述符上限是系统可以最大打开的文件数目
每个fd上面有callback(回调函数)函数,只有活跃的fd才会主动调用callback函数,不需要操作系统轮询
注意:epoll处理高并发,百万级,无需关心底层怎样实现
函数接口
#include <sys/epoll.h>
int epoll_create(int size);
//功能:创建红黑树根节点
/*
参数:
size:不作为实际意义值>0即可
返回值:
成功:返回epoll文件描述符
失败:-1
*/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
//功能:控制epoll属性
/*
参数:
epfd:epoll_create的返回句柄
op:表示动作类型。有三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
fd:需要监听的fd
event:告诉内核需要监听什么事件
EPOLLIN:表示对应文件描述符可读
EPOLLOUT:可写
EPOLLPRI:有紧急数据可读
EPOLLERR:错误
EPOLLHUP:被挂断
触发方式:边沿触发(默认使用)
ET模式:表示状态的变化
返回值:
成功:0
失败:-1
*/
//结构体
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event{
uint32_t events;/*epoll事件*/
epoll_data data;/*用户数据变量*/
};
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
//功能:等待事件的产生,类似于select的使用方法
/*
参数:
epfd:句柄
events:用来保存从内核得到事件的集合
maxevents:表示每次能处理的事件最大个数
timeout:超时时间,毫秒,0,立即返回,-1阻塞
返回值:
成功:返回发生事件的文件描述符个数
失败:-1
超时:0
*/
注意:
1.epoll可以同支持边缘触发和水平触发,理论上边缘触发性能更高,但是代码实现复杂
2.epoll同样只告知那些有序的文件描述符,放调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是代表一个就绪描述符数量的值
3.epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个描述符,一旦某个基于文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知
epoll实现IO多路复用的特点
监听的最大文件描述符没有个数限制
异步IO,当epoll有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
代码步骤示例
struct epoll_event event;//暂时保存关心的文件描述符及对应事件
struct epoll_event revents[20];//保存从链表中拿出来已经产生的事件
//创建表(创建红黑树)
int epfd=epoll_create(1);
if(epfd<0){
perror();
}
//将关心的文件描述符添加到树上
event.data.fd="文件描述符"
event.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,"文件描述符",&event);
event.data.fd="文件描述符"
event.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,"文件描述符",&event);
int ret;
char buf[128];
int recvbyte;
while(1){
//epoll_wait 阻塞等待链表中有事件产生,拿事件处理
ret=epoll_wait(epfd,revents,20,-1);
if(ret<0){
perror();
}
for(int i=0;i<ret;i++);
{
if(revents[i].data.fd=="文件描述符"){
}else if(revents[i].data.fd=="文件描述符"){
event.data.fd=acceptfd;
event.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,"文件描述符",&event);
}else{
if(……){
perror();
}else if(……){
close(event[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,revents[i].data.fd,NULL);
}
}
}
}
服务器模型
在网络程序里面,通常都是一个服务器处理多个客户机
为了处理多个客户机请求,服务器端的程序有不同的处理方式
循环服务器模型
同一个时刻只能响应一个客户端的请求
伪代码
socket()
bind()
listen()
while(1){
accept()
while(1){
process()
}
close()
}
并发服务器模型
同一时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型
多进程模型
每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型
伪代码
socket()
bind()
listen()
while(1){
accept()
if(fork()==0){
while(1){
process()
}
close(clientfd)
exit()
}else{
}
}
//注意:收到客户端消息后,打印是来自那个客户端的数据,使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源(子进程状态发生改变,会给父进程发送SIGCHLD信号)
多线程模型
每来一个客户端进行连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源少,属于使用比较广泛的模型
伪代码
socket()
bind()
listen()
while(1){
accept()
pthread_create()
}
IO多路复用模型
借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起来会比较繁琐