欢迎关注我的微信公众号【万能的小江江】
网络基础
TCP协议分为两个不同的协议
- 用来检测网络传输中差错的传输控制协议TCP(可靠传输)
- 专门负责对不同网络进行互联的的互联网协议IP(不可靠传输)
网络采用分层的思想
- 每一层实现不同的功能,对上层的数据做透明传输
- 每一层向上层提供服务,同时使用下层提供的服务
OSI七层模型
- OSI模型相关的协议目前比较少使用了,但是模型本身非常通用
- OSI模型是一个理想化的模型,尚未有完整实现
交换机
- 二层交换机(数据链路层)
- 三层交换机(网络层,比较偏软件)
TCP/IP协议族的体系结构
TCP/IP协议是Internet事实上的工业标准
- 底下三层属于Linux Kernel
- 应用层属于应用空间
网络接口和物理层(Link Layer)
屏蔽硬件差异
(每种外设会有不同的驱动,往上层走就会有统一的接口)
MAC地址
48位(12个数,每个数4位)的全球唯一的地址,是网络设备身份的标识
ARP/RARP协议
ARP
:通过IP地址找到MAC地址
RARP
:通过MAC地址找到IP地址
PPP协议
是一种拨号协议(GPRS/3G/4G
,传输介质不同了)
网络层(IP层,Internet Layer)
端到端的传输
IP协议
IP
:Internet Protocol(分为IPV4
和IPV6
)
ICMP
:Internet控制管理协议,ping命令``traceroute命令
(Windows下tracert
,pathping
)属于ICMP
IGMP
:Internet分组管理协议,广播、组播
传输层(Transport Layer)
数据应该交给哪个任务去处理
TCP协议的特点
Transfer Control Protocol,传输控制协议。提供面向连接的,一对一的可靠数据传输的协议(传输过程中会占用一些资源)
即数据无误、数据无丢失、数据无失序、数据无重复到达的通信
适用情况
- 适合对于传输质量要求较高,传输大量数据的通信
- 在需要可靠数据传输的场合,通常使用
TCP协议
- WeChat/QQ等即时通讯软件的用户登陆账户管理相关的功能通常采用TCP协议
UDP协议的特点
User Datagram Protocol,用户数据报协议。提供不可靠,无连接的尽力传输协议(在数据发送前不需要连接,实时性会更好)
适用情况
- 发送小尺寸数据(对DNS服务器进行IP地址查询的时候)
- 接收到数据时给出应答困难的网络中使用UDP(如:无线网络)
- 适用于广播/组播式通信中
- WeChat/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用
UDP协议
- 流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常可以采用UDP的方式进行实时的数据传输
SCTP协议
可靠传输,是TCP
的增强版,能实现多主机、多链路的通信,编程方法和TCP
不太一样
为什么可靠性比TCP高
它是多主机多链路传输,数据的存活性更高,可以防止传输过程中因为单条链路中断而导致数据传输的中断
应用层(Application Layer)
网页访问协议
HTTP/HTTPS
邮件发送、接收协议
POP3
(收)/SMTP
(发)
IMAP
(可接受邮件的一部分,没有把邮件下载下来,但是也可以处理这个邮件)
文件传输协议
FTP
远程登录协议
Telnet
(明文传输)/SSH
(加密传输)
嵌入式相关协议
网络时钟协议
NTP
简单网络管理协议
实现对网络设备集中式管理(路由器统一管理)
SNMP
(开源的,搜SNMPGet)
用传输音视频的协议
安防监控
RTP/RTSP
(这两个协议是基于TCP
+UDP
实现的)
互联网协议套组
TCP/IP协议通信模型
TCP/IP协议下的数据包(封包拆包)
以字节为单位
传输的时候是一个封包、拆包的过程
封包的时候从应用层开始,拆包的时候从链路层开始
MTU
Maximum Transmission Unit 最大传输单元
主要和网络的类型相关,以太网的MTU是1500
MSS
Maximum Segment Size 最大报文长度,最大分段大小(是一个静态的数据,一般指的是一个线路数据的大小)
是TCP协议定义的一个选项,MSS选项用于在TCP连接建立时,收发双方协商通信是每个报文段所能承载的最大数据长度
和网络类型、线路、系统特性相关(线路不同MSS也会不同)
一般是1460
TCP/IP网络编程预备知识
网络里面的通信是由IP地址+端口号来决定的
Socket
Socket可以跨过传输层(比如ICMP的ping命令,就只通过网络层,不通过传输层,如SOCK_RAW)
Socket简介
- 是一个应用编程接口
- 是一种特殊的文件描述符 (everything in Unix is a file)(可以对它执行IO的操作函数,比如
read()
,write()
,close()
等操作函数) - 代表着网络编程的一种资源
- 并不仅限于
TCP/IP
协议 - 面向连接(Transmission Control Protocol - TCP/IP)
- 无连接(User Datagram Protocol - UDP 和Inter-network Packet Exchange - IPX)
Socket类型
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送且按发送顺序接受
内设置流量控制,避免数据流淹没慢的接收方
数据被看作是字节流,无长度限制
数据报套接字(SOCK_DGRAM)
提供无连接服务,数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能被乱序接收原始套接字(SOCK_RAW)(对应着多个协议,发送穿透了传输层
可以对较低层次协议,如
IP
、ICMP
直接访问
IP地址
分为IPV4
和IPV6
IPV4
:采用32位
的整数(二进制)来表示
IPV6
:采用128位
的整数(二进制)来表示
MobileIPV6
:包括local IP
(本地注册的IP),roam IP
(漫游IP)
IPV4地址
点分形式:192.168.1.1
32位整数(二进制):11000000 10101000 00000001 00000001
特殊IP地址
局域网IP:192.xxx.xxx.xxx 10.xxx.xxx.xxx
广播IP:xxx.xxx.xxx.255 255.255.255.255(全网广播)
组播IP:224.xxx.xxx.xxx ~ 239.xxx.xxx.xxx
子网掩码:区分网段
端口号
16位的数字(1 ~ 65535)
为了区分一台主机接收到的数据包应该转交给哪个任务来处理,使用端口来区别
TCP和UDP的端口号独立
管理
端口号一般由IANA
(Internet Assigned Numbers Authority)管理
众所周知端口:1 ~ 1023(FTP:21 SSH:22 HTTP :80 HTTPS:443)(1 ~ 255之间为众所周知端口,256 ~ 1023端口通常由Unix系统占用)
保留端口:1024 ~ 5000(不建议使用)
可使用端口
注册端口:5000 ~ 49151
动态或私有端口:49152 ~ 65535(没有端口可以被正式地注册占用)
字节序
在不同CPU访问内存时,内存存储多字节数据有两种方法,称为主机字节序(HBO)(CPU访问的如果是字符串,则不存在大小端问题)
小端序(little - endian)低序字节存储在低地址
将低字节存储在起始地址,称为
Little-Endian
字节序,Intel和AMD,ARM等就是采用这种方式大端序(big - endian) 高序字节存储在低地址
将高字节存储在起始地址,称为
Big-Endian
字节序,由ARM作为路由器、Motorola、powerpc、mips等采用
网络传输中的数据必须按网络字节序,即大端字节序
在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;当应用进程从socket取出整数后,要转化成小端字节序
字节序转换函数
把给定系统采用的字节序称为主机字节序,为了避免不同类别的主机之间在数据交换时由于对字节序的不同而导致差错,引入了网络字节序
主机字节序到网络字节序
htonl (host to network long)
htons (host to network short)
- u_long htonl (u_long hostlong 4字节);
- u_short htons (u_short short 2字节);
网络字节序到主机字节序
ntohl (network to host long)
ntohs (network to host short)
- u_long ntohl (u_long hostlong);
- u_short ntohs (u_short short);
IP地址的转换
inet_addr()
可以
man inet_addr
查看函数用法功能同上,返回转换后的IPV4地址
int_addr_t inet_addr(const char *cp);
cp:点分形式的IP地址,结果是32位整数(内部包含了字节序的转换)
特点:
- 仅适用于IPV4
- 当初出错时返回-1
- 这个函数不能用于255.255.255.255的转换(涉及到补码)
inet_pton()
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
将IPV4/IPV6的地址转换成binary格式(二进制)
int iner_pton(int af, const char src, void dst);
特点:
- 适用于IPV4/IPV6
- 能正确处理255.255.255.255的转换问题
参数:
af
:地址协议族(AF_INET或AF_INET6)src
:是一个指针(填写点分形式的IP地址[主要指IPV4])dst
:转换的结果到dst
inet_ntop()
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
把IPV4/V6的网络字节序地址编程本地字符串形式的IP地址
特点:
- 适用于IPV4/IPV6
- 能正确处理255.255.255.255的转换问题
参数:
af
:地址协议族(AF_INET或AF_INET6)src
:是一个指针(填写32位的网络字节序IP地址)dst
:输出结果填写点分形式的IP地址[主要指IPV4]
返回值:
成功返回非空指针,空指针表示有错误
TCP编程
试着自己写一下这个程序,理解read为什么也会阻塞,通过进程或者线程如何进行并发
网络编程的5个函数(TCP编程API)
socket()函数
SYNOPSIS #include
/* See NOTES */ #include int socket(int domain, int type, int protocol); 参数:
domain
:#v4或者v6的 AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) #本地通信(进程间通信,两个一样的) AF_UNIX Local communication AF_LOCAL Synonym for AF_UNIX unix(7) #内核和用户界面通信 AF_NETLINK Kernel user interface device netlink(7) AF_PACKET Low level packet interface packet(7)
type
:SOCK_STREAM
:流式套接字 唯一对应于TCP
SOCK_DGRAM
:数据报套接字 唯一对应着UDP
SOCK_RAW
:原始套接字protocol
:一般填0 原始套接字编程时需要填充
返回值:
RETURN VALUE
On sucess,a file descriptor for the new socket is returned.On error,-1 is returned,and errno is set appropriately. #成功时返回文件描述符,出错时返回-1
bind()函数
(底下三个参数是到时候要填充的,但是我还是有点不太理解填充在哪)
sockfd
:通过socket()
函数拿到的fd
addr
:struct sockaddr的结构体变量的地址addrlen
:地址长度/* 绑定函数示例代码,IPV4 */ /* 如果是IPV6的编程,要使用struct sockaddr_in6结构体(详细情况查看man 7 ipv6),通常更通用的方法可以用struct sockaddr_storage来编程*/
#define SERV_PORT 5001 #define SERV_IP_ADDR "本机ip地址" int main(void) { int fd = -1; struct sockaddr_in_sin; /* 1.创建socket fd 创建套接字*/ if(fd = socket(AF_INET,SOCK_STREAM,0)) < 0{ perror("socket"); exit(1); }; /* 2.绑定 */ /* 2.1 填充struct sockaddr_in结构体变量 */ bzero(&sin,sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(SERV_PORT); //网络字节序的端口号 /* 方式1:只适用于ipv4 */ sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR); //结构体里面那个结构体,这里是把点分形式的ip地址转换成32位整数 inet_addr(const char *p) /* 方式2:*/ if(inet_pton(AF_INET,SERV_IP_ADDR,(void *强制转换)sin.sin_addr.s_addr) != 1){ perror("inet_pton"); exit(1); }; //inet_pton,成功返回1,没有有效字符串地址返回0或者-1 /* 2.2绑定 */ if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){ perror("bind"); exit(1); }; /* 3.调用listen()把主动套接字变成被动套接字 */ /* 4.阻塞等待客户端的连接请求 */ /* 5.读写 */ return 0; }
listen()函数
/* 3.调用listen()把主动套接字变成被动套接字,改变fd属性 */ if(listen(fd,BACKLOG) < 0){ perror("listen"); exit(1); } //出错的时候所占用的资源会被释放,前面要先define BACKLOG
int listen(int sockfd,int backlog); //backlog一般填5
参数:
sockfd
:通过socket()
函数拿到的fdbacklog
:同时允许几路客户端和服务器进行正在连接的过程(测试得知,ARM最大为8)
其他:
内核中服务器的套接字
fd
会维护2个链表:- 正在3次握手的客户端链表(数量 = 2 * backlog + 1,如果是5的话就是11路)
- 已经建立好连接的客户端链表(已经完成3次握手分配好了newfd)
比如:listen(fd,5); // 表示系统允许11 = (2*5+1)个客户端同时进行三次握手
返回值:
成功返回
0
,出错返回-1
accept()函数
/* 4.阻塞等待客户端的连接请求,因为是个系统应用?用man 2 accept查手册 */ int accept(int sockfd,struct sicjaddr *addr,socklen_t *addrlen); //sockfd是被前面的处理过的,后面两个变量会取到客户端的信息(可以获取到IP地址和端口号((前提是传入的数据有相关信息)
newfd = accept(fd,NUll,NULL); if(newfd < 0){ perrot("accept"); exit(1); }
#define QUIT_STR "quit" #include <unistd.h> #include <stdlib.h> #incldue <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <errno.h> //与errno相关 /* 5.读写 */ //..和newfd进行数据读写 //...FIXME!!!(已知无法执行的代码的意思) int ret = -1; char buf[BUFSIZ]; while(1){ bzero(buf, BUFSIZ); do{ read(newfd, buf, BUFSIZ); }while(ret < 0 && EINTR == errno); if (ret < 0){ perror("read"); exit(1); } if (ret == 0){ //对方已关闭 break; } printf("Receive data: %s\n",buf); if (strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)){ //用户输入了quit子政府 printf("Client is exiting!\n"); break; } } close(newfd); close(fd); return 0;
参数:
sockfd
:经过前面socket()
创建并通过bind()``,listen()
设置过的fd
addr
和addrlen
:
返回值:
成功时返回已经建立好连接的新的
newfd
,出错的时候返回-1
(可以有很多个newfd,看建立多少个连接,newfd 1 2 3 …)connect()函数
连接函数
SYNOPSIS #include <sys/types.h> //See Notes #include <sys/socket.h> int connect(int sockfd,const struct sockaddr *addr, socklen_t addrlen);
if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){ perror("connect"); exit(1); }
connec()函数和bind()函数的写法类似
参数:
sockfd
:通过socket()
函数拿到的fd
addr
:struct sockaddr
的结构体变量的地址addrlen
:地址长度
返回值:
连接成功返回
0
,出错返回-1
/* net.h */ /* 把常用头文件放到这里 */ #ifndef __MAKEU_NET_H__ #define __MAKEU_NET_H__ #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #incldue <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <errno.h> #define SERV_PORT 5001 #define SERV_IP_ADDR "本机IP" #define BACKLOG 5 #define QUIT_STR "quit" #endif
/* 总的程序 */
/* 需要实现在client(客户端)发送数,在sever(服务端)可以实时显示接收数据 */
#include "net.h"
int main(void)
{
int fd = -1;
struct sockaddr_in sin;
/* 1.创建socket_fd 创建套接字 */
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("socket");
exit(1);
}
/* 2.绑定 */
/* 2.1 填充struct sockaddr_in结构体变量 */
bzero(&sin,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT); //网络字节序的端口好
/* 优化1:让服务器程序能绑定在任意的IP上 */
#if 1
sin.sin_addr.s_addr = htonl(INADDY_ANY); //INADDY_ANY是-1,在大端小端模式下是一样的
#else
if(inet_pton(AF_INET,SERV_IP_aDDR,(void *)&sin.sin_addr) != 1){
perror("inet_pton");
exit(1);
}
}
#endif
/* 2.2 绑定 */
if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){
perror("bind");
exit(1);
}
/* 3.调用listen()把主动套接字变成被动套接字 */
if(listen(fd,BACKLOG) < 0){
perror("listen");
exit(1);
}
printf("Server starting...OK!\n");
int newfd = -1;
/* 4.阻塞等待客户端连接请求 */
#if 0
newfd = accept(fd,NULL,NULL);
if(newfd < 0){
perror("accept");
exit(1);
}
#else
/* 优化2:通过程序获取刚建立连接的socket的客户端IP地址和端口号 */
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0){
perror("accept");
exit(1);
}
char ipv4_addr[16];
if(!inet_ntop(AF_INET,(void *)cin.sin_addr,ipv4_addr,sizeof(cin))){
perror("inet_ntop");
exit(1);
}
printf("Client(%s:%d) is connected!\n",ipv4_addr,ntons(cin.sin_port));
#endif
/* 5.读写 */
//..和newfd进行数据读写
int ret = -1;
char buf[BUFSIZ];
while(1){
bzero(buf,BUFSIZ);
do{
ret = read(newfd,buf,BUFSIZE-1);
}while(ret < 0 && EINTR == errno);
if(ret < 0){
perror("read");
exit(1);
}
}
并发服务器
可以通过多线程或者多进程的方式来做(旧的服务器不能并发的原因:accept()函数被阻塞,read()函数阻塞)
所以要在accept的地方进行一个优化
这个课程感觉不够系统,之后的笔记就不继续更新了