文章目录
- 一、TCP协议
- 1.ISO网络模型及其功能
- (1)ISO七层网络模型及其功能:学术界使用
- (2)五层网络模型:学术界使用
- (3)四层网络模型:工业界使用
- 2.TCP协议
- (1)TCP头部协议需要注意的地方
- (2)TCP3次握手与4次挥手
- (a)基本知识点1:TCP建立连接时:须知:通信的双方要互相确认对方的最大报文长度( MSS )
- (b)基本知识点2:TCP关闭连接时,须知:TCP半关闭概念,即TCP的连接是全双工的
- (c)基本知识点3:为什么建连接要 3 次握手,断连接需要 4 次挥手?
- (d)三次握手与四次挥手
- (3)TCP的连接状态流转图
- 3.TCP超时重传:保证可靠服务
- (1)网络常见的异常情况
- (2)超时重传的含义
- (3)telnet和tcpdump演示TCP超时重传
- tcpdump -i 指定网卡口 'port 端口号'
- telnet ip地址 端口号
- (a)超时重传时间:RTO与RTT
- (4)TCP 渭动窗口:提供可靠性,以及流控特性
- (5)TCP拥塞控制
- 二、TCP网路编程API
- 1.如何标识主机中的应用程序(进程)?
- 2.TCP协议通信的socket的实例
- (1)socket函数
- (2)bind函数
- (3) listen 和 connect 函数
- (4)accept 函数
- (5)read 和 write 函数
- (6)close函数
- 3.实现一个TCP server和client
一、TCP协议
1.ISO网络模型及其功能
(1)ISO七层网络模型及其功能:学术界使用
七层网络模型的数据封装与解封装
- 当一台主机需要传送用户的数据( data ) 时,数据首先通过应用层的接口进入应用层 。
- 在应用层,用户的数据被加上应用层的报头( Ppplication Header,AH ),形成应用层协议数据单元( Protocol Data Unit, PDU ),然后被递交到下层表示层
- 表示层并不“”关心“”上层应用层的数据格式而是把整个应用层递交的数据包看成是一个整体(应用层数据)进行封装,即加上表示层的报头( Presentation Header, PH )。 然后,递交到下层会话层 。
- 同样,会话层 、 传输层 、 网络层(假设用 TCP 传输,则是 TCP 数据+ IP 包头)、数据链路层(把上层的 TCP 数据+ IP 包头统一称为帧数据,即帧头 +帧数据+帧尾( CRC ))也都要分别给上层递交下来的数据加上自己的报头 。
- 补充:会话层报头( Session Header ,SH )、传输层报头( Transport Header, TH )、 网络层报头( Network Header, NH )和数据链路层报头( Data link Header , DH ) 。 其中,数据链路层还要给网络层递交的数据加上数据链路层报尾( Data link Termination, DT)形成最终的一帧数据 。
(2)五层网络模型:学术界使用
(3)四层网络模型:工业界使用
- TCP/IP协议族并不包含物理层和数据链路层,因此它不能独立完成整个计算机网络系统的功能
-
网络接口层:用于协作 IP 数据在已有网络介质上传输的协议。 实际上 TCP/IP 标
准并不定义与 ISO 数据链路层和物理层相对应的功能 。 相反,它定义了像 ARP 这样的协议,提供 TCP/IP 协议的数据结构和实际物理硬件之间的接口 - 网络层:本层包含 IP 协议、RIP 协议,负责数据的包装 、 寻址和路由 。同时还包含 ICMP(Internet Control Message Protocol ,网间控制报文协议)用来提供网络诊断信息。
- 传输层: TCP协议提供可靠的数据流运输服务, UDP 协议提供不可靠的用户数据报服务
- 应用层: 因特网的应用层协议包括 Finger 、Whois 、 FTP(文件传输协议)、Gopher 、 HTTP (超文本传输协议)、 Telent (远程终端协议)、SMTP(简单邮件传送协议)、 IRC (因特网中继会话)、阳叫TP (网络新闻传输协议)等。相当于七层中的应用层和表示层。
- 总结:
所有程序的数据首先会打包到 TCP 的 Segment中,然后 TCP 的 Segment 会打包到 IP 的 Packet 中 ,然后再打包到以太网 Ethernet 的 Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。
2.TCP协议
(1)TCP头部协议需要注意的地方
(2)TCP3次握手与4次挥手
- 其实,网络上的传输是没有连接的, TCP 也是一样的 。 而 TCP 所谓的 “连接”,其实只不过是在通信的双方维护一个“连接状态”,让它看上去好像有连接一样 。 所以, TCP 的状态变换是非常重要的 。
(a)基本知识点1:TCP建立连接时:须知:通信的双方要互相确认对方的最大报文长度( MSS )
(b)基本知识点2:TCP关闭连接时,须知:TCP半关闭概念,即TCP的连接是全双工的
(c)基本知识点3:为什么建连接要 3 次握手,断连接需要 4 次挥手?
(d)三次握手与四次挥手
(3)TCP的连接状态流转图
- 客户端和服务器端都可以用此图解释,下面表示的是其各个状态,看上面的三次握手和四次挥手就明白了这个状态为啥不区分用户端和服务器端了
3.TCP超时重传:保证可靠服务
(1)网络常见的异常情况
(2)超时重传的含义
(3)telnet和tcpdump演示TCP超时重传
tcpdump -i 指定网卡口 ‘port 端口号’
telnet ip地址 端口号
- 其中, telnet ip port 是查看某一个机器上的某一个端口是否可以访问 。 tcpdump-i ethl 'port 1055’是用来抓取网卡eth1 上的1055 端口上的包,如图 6-6 所示 。
- 等了一会,没有其他请求干扰后,就可以再开一个终端,并执行 telnet 218.111.111.111 .
1055 命令,探测 218.111.111.111 1055 是否可以连得通,如图 6-7 所示 。过了一会,可以看到如图 6-8 所示的界面 。说明连接到 218.111.111.111 1055 失败了 。 - 这时可以看到,再把 tcpdump 的那个终端按 Ctrl+C 结束掉 tcpdump 命令 。 可以看到抓
包结果如图 6-9 所示 。
(a)超时重传时间:RTO与RTT
那这个超时重传时间一般设置为多少才合适呢?
影响超时重传机制协议效率的一个关键参数是 RTO ( Retransmission Timeout,重传超时时间 。 RTO 指发送端发送数据后、重传数据前等待接受方收到该数据报文的 ack 时间。
RTO 的设置对于重传非常重要:
1 )设长了,重发就慢,没有效率,性能差;
2 )设短了,重发得就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发 。
3)在实现端到端的通信时,不同端点之间传输通路的性能可能存在着巨大的差异,而且同一个 TCP 连接在不同的时间段上,也会由于不同的网络状态具有不同的传输时延。为了处理这种底层网络传输特性的差异性和变化性,TCP 协议使用自适应算法( AdaptiveRetransmission Algorithm)以适应互联网分组传输时延的变化。 这种算法的基本要点是 TCP监视每个连接的性能(即传输时延),由每一个 TCP 的连接情况推算出合适的 RTO 值, 当连接时延性能变化时, TCP 也能够相应地自动修改 RTO 的设定,以适应这种网络的变化。
为了动态地设置, TCP 引入了 RTT ( Round Trip Time),也就是连接往返时间,指发送端从发送 TCP 包开始到接收它的立即响应所耗费的传输时间 。
一个连接而言,若能够了解端点间的 Rπ (传输往返时间),则可根据 RTT 来设置一合适的 RTO 。 显然,在任何时刻连接的 RTT 都是随机的,无法事先预知 。TCP 通过测量来获得连接当前 RTT 的一个估计值,并以该 RTT 估计值为基准来设置当前的 RTO。自适应重传算法的关键就在于对当前 RTT 的准确估计,以便适时调整 RTO 。
(4)TCP 渭动窗口:提供可靠性,以及流控特性
- TCP 的窗口是一个 16bit 位字段它代表的是窗口的字节容量, 也就是 TCP 的标准窗口最大为 216-1=65535 个字节
-
TCP 是双工的协议,会话的双方都可以同时接收、发送数据 。 TCP 会话的双方都各自维护一个“发送窗口”和一个“接收窗口” 。 其中各自的“接收窗口”大小取决于应用 、 系统、
硬件的限制(TCP 传输速率不能大于应用的数据处理速率) 。 各自的“发送窗口”则要求取决于对端通告的“接收窗口” -
滑动窗口实现面向流的可靠性来源于“确认重传”机制 。 TCP 的滑动窗口的可靠性也是
建立在“确认重传”基础上的 。 发送窗口只有收到对端对于本段发送窗口内字节的 ACK 确
认,才会移动发送窗口的左边界。 接收窗口只有在前面所有的段都确认的情况下才会移动左边界
(5)TCP拥塞控制
- 拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。
- 拥塞控制是一个全局性的过程,和流量控制不同,流量控制指点对点通信量的控制 。
- TCP 的拥塞控制由 4 个核心算法组成:慢开始( Slow Start )、 拥塞避免(Congestion Voidance )、快速重传( Fast Retransmit)和快速恢复( Fast Recovery)
1.如何标识主机中的应用程序(进程)?
- 网络层的 IP 地址可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组 (Ip 地址,协议,端口)就可以标识网络的进程了, 网络中的进程通信就可以利用这个标志与其他进程进行交互 。
- 网络中的进程是通过 socket 来通信的, socket 是“ open-write/read-close 模式的一种实现,socket即是一种特殊的文件, 一些 socket 函数就是对其进行的操作(读/写 、 打开、关闭)
2.TCP协议通信的socket的实例
- 服务器 socket 和客户端 socket 建立连接的部分其实就是大名鼎鼎的 3 次握手
- TCP 服 务器端依次调用 socket()、 bind() 、 listen()之后,就会监听指定的 socket 地址了 。TCP 客户端依次调用 socket() 、 connect()之后就会向 TCP 服务器发送了一个连接请求 。TCP 服务器监听到这个请求之后,就会调用 accept()函数取接收请求,这样连接就建立好了 。
(1)socket函数
- socket 的函数原型如下所示:
int socket(int domain , int type , int protocol) ; - socketO 用于创建一个 socket 描述符( socket descriptor),它唯一标识一个 socket。 这个 socket 描述符与文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
-
正如可以给 fopen 的传人不同参数值,以打开不同的文件一样,创建 socket 时,也可以
指定不同的参数创建不同的 socket 描述符, socket 函数的 3 个参数分别如下所述。
(2)bind函数
- 正如上面所说 bind() 函数把一个地址族中的特定地址赋给 socket。 例如对应 AF INET,AF_INETT6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket 。
- bind 的 函数原型是 :
int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
(3) listen 和 connect 函数
- 如果作为一个服务器,在调用 socket()、 bind()之后就会调用 listen()来监昕这个socket,如果客户端这时调用 connect()发出连接请求,服务器端就会接收到这个请求
- listen 和connect 的函数原型是:
int listen(int sockfd , int backlog) ;
int connect(int sockfd , const struct sockaddr *addr, socklen_t addr len ) ;
(4)accept 函数
-
建立好连接后,就可以开始网络 I/0 操作了,即类同于普通文件的读写 I/0 操作。 accept 的函数原型是 :
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrl en) ;
(5)read 和 write 函数
- 至此服务器与客户已经建立好连接了,可以调用网络 I/0 进行读写操作了,即实现了网络中不同进程之间的通信
- 网络 I/0 操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfom()/sendto() - 最常用的是read()和write()
(6)close函数
- 完成了读写操作就要关闭相应的socket 描述字,好比操作完打开的文件要调用 close 关闭打开的文件。
- 需要包含的头文件是:
#include <unistd std.h>
- close 的函数原型是:
int close (int fd) ;
3.实现一个TCP server和client
下面用 TCP 协议编写一个简单的服务器、客户端,其中服务器端一直监听本机的 6666号端口 。如果收到连接请求,将接收请求并接收客户端发来的消息 ; 客户端与服务器端建立连接并发送一条消息。
server端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[4096];
int n;
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )//建立一个socket描述符,socket(ipv4,提供面向连接的稳定传输数据,指定协议为0)
{
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));//先把servaddr地址清空,再复制
servaddr.sin_family = AF_INET;//ipv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//AF_INET表示什么IP都可以连上
servaddr.sin_port = htons(6666);
if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)//将servaddr地址绑定到该socket描述符
{
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
if( listen(listenfd, 10) == -1)//监听这个socket描述符
{
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
printf("======waiting for client's request======\n");
while(1)
{
if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1)
{
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
n = recv(connfd, buff, MAXLINE, 0);// 注意, recv 函数接收到的字符串是不带 ”\0”结束符的
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);,//要用 printf输出时必须得先加上结束符”\0"
close(connfd);
}
/*
在 while 循环里持续接收包,注意, accept 和 recv 是都是在 while 循环里的,也就
是收到包之后,listenfd这就没用了,并关闭它,下一个包重新接收包 。
*/
close(listenfd);
return 0;
}
- client端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int sockfd, n;
char recvline[4096], sendline[4096];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
return 0;
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)// arg[1]是写ip地址的地方,inet_pton 是 IP 地址转换函数,可以在将 IP 地址在“点分十进制”和“二进制整数”之间转换
{
printf("inet_pton error for %s\n",argv[1]);
return 0;
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
close(sockfd);
return 0;
}
- makefile文件
all:server client
server:server.o
g++ -g -o server server.o
client:client.o
g++ -g -o client client.o
server.o:server.cpp
g++ -g -c server.cpp
client.o:client.cpp
g++ -g -c client.cpp
clean:all
rm all
运行如下: