交换机与路由器: 交换机:用于局域网内网的数据转发 路由器:用于连接局域网和外网
IP地址:
- IP地址是Internet中主机的标识
- Internet中的主机要与别的机器通信必须具有一个IP地址
- IP地址为32位(IPv4)或者128位(IPv6)
- 每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由
- 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。 192.168.1.31 C类 , 点分十进制IP
C0A8011F 4*8 = 32位
IP分类:
IP地址根据网络号和主机号来分,分为A 、B 、C 三类及特殊地址D 、E 。 全0和全1的都保留不用。
-
A类: (1.0.0.0-126.0.0.0)(默认子网掩码:255.0.0.0或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类IP地址的最前面为“0”,所以地址的网络号取值于1~126之间。一般用于大型网络。 -
B类: (128.0.0.0-191.255.0.0)(默认子网掩码:255.255.0.0或0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。一般用于中等规模网络。 -
C类: (192.0.0.0-223.255.255.0)(子网掩码:255.255.255.0或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。一般用于小型网络。 -
D类: 是多播地址。该类IP地址的最前面为“1110”,所以地址的网络号取值于224~239之间。一般用于多路广播用户 E类:是保留地址。该类IP地址的最前面为“1111”,所以地址的网络号取值于240~255之间。 IP=网络号+主机号; 理论IP地址范围:
A类:1.0.0.0 - 126.255.255.255 B类:128.0.0.0 - 191.255.255.255 C类:192.0.0.0 - 223.255.255.255 D类:224.0.0.0 - 239.255.255.255
- 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;目的是合理的利用IP资源;
- 子网掩码长度是和IP地址长度完全一样的32bit的二进制数组成;
- 前半部分全1(连续1),后半部分全0;
- 例如:
C类地址,同一网段最多可以连接多少个主机??? 192.168.1.0 - 192.168.1.255 => 256 (最多可以连接主机的个数为254,除网络地址0,广播地址255) - 网络概念:
网络号相同的主机组成一个局域网,局域网可以理解为是一个小型网络。 若干个小型网络组合在一起可以组合成大型网络 => 以太网 Inetnet
【1】OSI模型与TCP/IP协议体系结构
网络的体系结构
- 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
- 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
- 网络体系结构即指网络的层次结构和每层所使用协议的集合
- 两类非常重要的体系结构:OSI与TCP/IP
OSI开放系统互联模型
- OSI模型是一个理想化的模型,尚未有完整的实现
- OSI模型共有七层
OSI模型
OSI模型是最理想的模型
- 物理层:传输的是bit流(0与1一样的数据),物理信号,没有格式
- 链路层:格式变为帧(把数据分成包,一帧一帧的数据进行发送)
- 网络层:路由器中是有算法的,ip,(主机到主机)(路由的转发)
- 传输层:端口号,数据传输到具体那个进程程序 (端到端)
- 会话层:通信管理,负责建立或者断开通信连接
- 表示层:确保一个系统应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据加密,解密;
- 应用层:指定特定应用的协议,文件传输,文件管理,电子邮件等。
【2】 TCP/IP协议族
- 应用层 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
- 传输层 TCP,UDP
- 网络层 IP,ICMP,RIP,OSPF,BGP,IGMP
- 网络接口与物理层 SLIP,CSLIP,PPP,ARP,RARP,MTU ISO2110,IEEE802.1,EEE802.
【3】UDP TCP 协议相同点
都存在于传输层(*****)
【4】三种socket
sockfd = 3 是一个编程接口;返回一种特殊的文件描述符 (everything in Unix is a file)
-
流式套接字(SOCK_STREAM) TCP
- 提供了一个面向连接、可靠的数据传输服务,
- 数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,
- 避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
-
数据报套接字(SOCK_DGRAM) UDP
- 提供无连接服务。数据包以独立数据包的形式被发送
- 不提供无差错保证,数据可能丢失或重复,
- 顺序发送,可能乱序接收。
-
原始套接字(SOCK_RAW)
- 可以对较低层次协议如IP、ICMP直接访问。
【5】端口号(vi /etc/services)
-
为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别 -
TCP端口号与UDP端口号独立 -
端口号一般由IANA (Internet Assigned Numbers Authority) 管理 众所周知端口:1~1023 (1~255之间为众所周知端口, 256~1023端口通常由UNIX系统占用) 已登记端口:1024~49151 动态或私有端口:49152~65535 一般使用:6666 8888 7777 9999 10000 10001(5000+) sin_port 不同类型CPU的主机中,内存存储多字节整数序列有两种方法, 称为主机字节序(HBO): -
小端序(little-endian) - 低序字节存储在低地址 -
大端序(big-endian)- 高序字节存储在低地址
网络中传输的数据必须按网络字节序,即大端字节序 在大部分PC机上,当应用进程将整数送入socket前, 需要转化成网络字节序;当应用进程从socket取出整数后, 要转化成小端字节序。
-
主机字节序转化网络字节序? -
小端 转 大端 inet_addr() -
由主机字节序转化为网络字节序(大端),返回转换后的地址。 in_addr_t inet_addr(const char *strptr); inet_ntoa() -
将32位网络字节序二进制地址转换成点分十进制的字符串。 char *inet_ntoa(stuct in_addr inaddr); -
主机字节序到网络字节序 u_long htonl (u_long hostlong); u_short htons (u_short short); -
网络字节序到主机字节序 u_long ntohl (u_long hostlong); u_short ntohs (u_short short); //short 2字节
【6】 TCP编程流程
-
服务器端:
socket() ,创建套接字文件,用于连接 sockfd (有一个属性默认是阻塞)bind() , 绑定,把socket() 函数返回的文件描述符和IP、端口号进行绑定;listen() , (监听)将socket() 返回的文件描述符的属性,由主动变为被动;accept(), 阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,则accept() 函数返回,返回一个用于通信的套接字文件;recv() , 接收客户端发来的数据;send() , 发送数据;close() , 关闭文件描述符;连接、通信 -
客户端:
socket() ,创建套接字文件,既用于连接,也用于通信; 完成一个结构体的填充connect() ; 用于发起连接请求;send() , 发送数据;recv() , 接收数据;close() , 关闭文件描述符;
1.socket()
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能: 创建套接字文件
参数:
domain:协议族
AF_UNIX, AF_LOCAL 用于本地通信
AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols
type:协议类型
SOCK_STREAM TCP
SOCK_DGRAM UDP
protocol:
一般情况下写0
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功: 返回一个特殊文件描述符;
失败: -1
2.bind()
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能: 绑定,将socket()返回值和IP/端口号进行绑定;
(以什么样的形式去绑定?就是填充第二个结构体,把端口号和IP填充到这个结构体中)
参数:
sockfd: 是socket()函数的返回值;
const struct sockaddr *addr:
struct sockaddr是结构体类型,是一个通用结构体;
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
整个结构体大小为16个字节
(程序员每次填充的时候填充自己的结构体,将自己的结构体强转成通用的结构体,因为有机器之间通信,有本地通信。struct sockaddr_in是Internet的结构体,本地通信还会有本地通信所要填充的结构体sockaddr_un,每种协议都有自己需要填充的一个结构体,如果每种协议都有自己的函数接口的话,函数接口太多,没办法记忆,为了做到统一性,填充的填充自己的结构体,传值的时候传struct sockaddr,那么就需要把自己填充的sockaddr_in强制转换成struct sockaddr形式)
******在填充的时候填充struct sockaddr_in ;
struct sockaddr_in {
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
struct in_addr {
__be32 s_addr;
};
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
addrlen:
结构体的大小;
sizeof(serveraddr);
返回值:
-1 失败
3.listen()
int listen(int sockfd, int backlog);
功能: 用于监听,将主动套接字变为被动套接字;
参数:
sockfd: socket()的返回值
backlog:客户端同时(1s)连接服务器的最大个数;
(队列1:保存正在连接)
(队列2,连接上的客户端)
返回值:
失败 -1
4.accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
sockfd
addr: 如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值:
文件描述符;
acceptfd;
5.recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd ;
buf 存放位置
len 大小
flags 一般填0,相当于read()函数
MSG_DONTWAIT
返回值:
< 0 失败出错
==0 表示客户端退出
>0 成功接收的字节个数
6.send()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd
buf
len
flags 如果填0,相当于write();
7.close()
【5】UDP
-
无连接,不可靠的传输协议; UDP编程流程: 服务器端:
-
socket(),返回一个文件描述符,用于通信 对于TCP是先运行服务器,完成结构体的填充sockaddr_in -
bind(); 绑定 服务器需要将自己的IP和端口号与用于通信的文件描述付进行绑定 -
recvfrom(), 接收数据 -
sendto(), 发送数据 可以把sendto替换成send但是需要在前面加connect用于确定send要将数据发给谁 -
close(sockfd); 客户端: 1. socket(), 返回一个文件描述符,用于通信 2. 填充结构体,(目的是告诉程序,数据要发送给谁,及填充结构体的时候填充serveraddr) 3. sendto() 4. recvfrom() 5. close(); send recv sendto recvfrom
【6】
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
第5/6参数,明确接收哪个客户端发来的数据;
【7】
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
第5/6参数,明确数据要发送给谁;
- 备注:
- 对于TCP是先运行服务器,客户端才能运行。
- 对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系。
- 一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
- UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
- 在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。
【1】UNIX/Linux下主要的四种IO模型的特点
-
阻塞式IO :最简单、最常用;效率低 阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。 缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。 前面学习的很多读写函数在调用过程中会发生阻塞。 ? 读操作中的read、recv、recvfrom ? 写操作中的write、send ? 其他操作:accept、connect
- 读阻塞
以read函数为例: 进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发生阻塞。 它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。 经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据。 如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。
- 写阻塞
在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。 这时,写操作不进行任何拷贝工作,将发生阻塞。 一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。 UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。 -
非阻塞式IO :可以处理多路IO;需要轮询,浪费CPU资源 ? 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。” ? 当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。 ? 应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。 ? 这种模式使用中不普遍。 例如: Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞。 非阻塞,循环检测,是否有数据发过来,轮询消耗CPU资源。 -
O多路复用 :服务器可以响应多个客户端发来的数据。 -
信号驱动IO :异步通知模式,需要底层驱动的支持
【2】fcntl
通过该函数设置文件描述符的属性
int fcntl(int fd, int cmd, long arg);
int flag;
flag = fcntl(0, F_GETFL); // 1.获取该文件描述符的原属性
flag |= O_NONBLOCK; //2. 修改对应的位
fcntl(0, F_SETFL, flag); // 3. 写回去
【3】I/O多路复用
应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的; 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间; 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂; 比较好的方法是使用I/O多路复用。其基本思想是: ? 先构造一张有关描述符的表,然后调用一个函数。 ? 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。 ? 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。 select poll epoll
- 先构造一张有关文件描述符的表(集合、数组);
- 将你关心的文件描述符加入到这个表中;
- 然后调用一个函数。 select / poll
- 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候
该函数才返回(阻塞)。 - 判断是哪一个或哪些文件描述符产生了事件(IO操作);
- 做对应的逻辑处理;
- 注意:
****select函数返回之后,会自动将除了产生事件的文件描述符以外的位全部清空; 这样当你想下一次监听其它的事件的话,都被清空了,就监听不到了,所以写代码的时候,要定义一个临时的集合,所以在调用select之前要把原有的readfds付给临时tempfds。 任务: 我想检测是键盘事件(标准输入 文件描述如为0 ), 还是鼠标事件(文件描述符是/dev/input/mouse2);
【4】select
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#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
select返回值: <0 出错
>0 表示有事件产生;
如果设置了超时检测时间:&tv
select返回值:
<0 出错
>0 表示有事件产生;
==0 表示超时时间已到;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set); //将set集合中的fd清除掉
int FD_ISSET(int fd, fd_set *set); //判断fd是否存在于set集合中
void FD_SET(int fd, fd_set *set); //将fd加入到集合中
void FD_ZERO(fd_set *set); //清空集合
【1】IO多路复用 (并发服务器)
- 想实现服务器处理多个客户端连接请求或数据收发的话,(实现并发)
- 多进程的方式;
- 多线程的方式;
- IO多路复用
- select
? 一个进程最多只能监听1024个文件描述符 (千级别) ? select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源); ? select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程04G,03G是用户态,3G~4G是内核态,拷贝是非常耗时的); - poll
? 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组的大小为1,如果想监听100个,那么这个结构体数组的大小就为100,由程序员自己来决定) ? poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低 ? poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可 - epoll
? 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统) ? epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高 ? epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:这个函数是某些Unix系统提供的用于执行与select()函数同等功能的函数
参数:
`struct pollfd *fds`
关心的文件描述符数组struct pollfd fds[N];
每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便
nfds: nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量;
每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便。
`timeout`: 超时检测
毫秒级的:如果填1000,1秒
如果-1,阻塞
返回值:
>0:返回已就绪的文件描述符数
==0:超时
-1: poll函数调用失败,同时会自动设置全局变量errno;
创建一个结构体数组
struct pollfd fds[2];
将你关心的文件描述符加入到结构体成员中
struct pollfd {
int fd;
short events;
short revents;
则会自动填充该成员的值(当文件描述符产生事件之后,
这个函数会自动的把读事件或者写事件填充到这个结构体revents的成员里面.
如果产生读事件,就给revents一个读事件,如果写事件,一样就给revents一个写事件。
)
};
fds[0].fd = 0;
fds[0].events = POLLIN; (POLLIN表示读事件)
fds[1].fd = mouse1_fd;
fds[1].events = POLLIN;
把关心的文件描述符添加到集合当中,把关心的事件也要添加进来。
3. 调用poll函数
如果返回表示有事件产生;
poll(fds,2,1000)
4. 判断具体是哪个文件描述符产生了事件
if(fds[0].revents == POLLIN)
{ ....
}
- .
epoll ? 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统) ? epoll 当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高 ? epoll 不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可. 注意: Epoll 处理高并发,百万级,不关心底层怎样实现,只需要会调用就OK。
解释:Epoll 的底层实现如上图:(了解) ? (红黑树,是特殊的二叉树,Epoll怎样能监听很多个呢?首先创建树的根节点,每个根节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,包含callback函数),对于树来说可以随意挂接节点。 ? 链表,当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件),链表为事件链表。
- 3个功能函数:
#include <sys/epoll.h>
-
int epoll_create(int size); 功能:创建红黑树根节点(创建epoll实例) 返回值:成功时返回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 :被挂断; EPOLLET :触发方式,电平触发; 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_t data; / *用户数据变量* / }; //等待事件到来 -
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 功能:等待事件的产生,类似于select的用法 epfd :句柄; events :用来从内核得到事件的集合; maxevents :表示每次能处理事件最大个数; timeout :超时时间,毫秒,0立即返回,-1阻塞 //成功时返回发生事件的文件描述数,失败时返回-1
|