网络通讯需要注意(协议 + IP地址) + OSI + TCP/IP + 通讯传输流程
局域网, 广域网,互联网,域域网,因特网
port端口: 在一台主机上标识一个进程
协议: 双方通讯的协定
网络协议: 网络通信唤醒数据的约定格式
通信协议标准: 网络互联的前提
协议分层:
协议封装,便于使用
对服务,接口,协议进程明确的划分
OSI七层模型:
通信特点:对等通信
对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信.
TCP/IP协议
物理层: 负责光电信号的传输; Ethernet; 以太网协议; 集线器(信号放大)
链路层: 负责相邻设备之间的数据帧传输; Ethernet(以太网)数据的结束开始; 交换机
网络层: 负责地址管理与路由选择; IP; 路由器
传输层: 负责端与端之间的数据传输; TCP UDP
应用层: 负责应用程序之间的数据沟通; http ftp(传输文件) SYMP DNS
IP地址
IPV4: unit_t – 在网络唯一标识一台主机
IPV6: uchar ip[16] – 不向兼容
DHCP: 动态地址分配
NAT: 地址替换-- 实现多人共IP上网
使用点分十进制展示IP地址: 127.0.0.1 本地回环IP
每条数据包括: SRC IP DEST IP --标识了这条数据从哪来到哪去
port端口
0 ~ 65535–其中0 ~1024不推荐使用-在主机上标识一个进程
每条数据中包括: sip sport dip dport proto(五元组–标识一条通讯)
网络字节序
在下方tcpsocket.cpp程序中体现
应用层
http协议(超文本传输协议)
URl
俗称"网址"
按照网址顺序依次为:
协议方案名 -> 登录信息 -> 服务器地址 -> 服务器端口号 -> 带层次的文件夹路径 -> 查询字符串 -> 片段标识符
urlencode和urldecode
/ ? %等字符在url中被赋予特殊意义在遇到此类字符需要进行转义
转义规则:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式,例如: ‘+‘被转义为’%2B’
urldecode就是urlencode的逆过程
http协议格式
http请求
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性,冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body.Body允许为空字符串. 如果Body存在, 则在Header中会有一个
Content-Length属性来标识Body的长度;
http响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性,冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body.Body允许为空字符串. 如果Body存在, 则在Header中会有一个 Content-Length属性来标识Body的长度;如果服务器返回了一个html页面, 那么html页面内容就是在 body中.
http的方法
最常用的是GET方法和POST方法
http的状态码
最常见的状态码,比如200(OK),404(Not Found), 403(Forbidden),301(永久重定向,location所指向的位置)302(Redirect,临时 重定向), 504(Gateway time-out) 500(服务器内部错误) 502(Bad Gateway)
http的常见header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
封装实现最简单的http服务器
1 /**********************************************************
2 * Author : yang
3 * Email : wk_eternity@163.com
4 * Last modified : 2019-07-22 15:54
5 * Filename : httpServer.cpp
6 * Description : 使用封装的tcpsocket完成实现一个最简单的http服务器
7 * 返回一个简单页面:
8 * <html><body><h1>Hello World</h1></body></html>
9 * http是传输层协议,在传输层使用tcp协议实现;默认端口80
10 * tcp服务器的基础上进行http协议数据的解析
11 * http是一个短连接,在http1.1中实现了长连接
12 * http数据的解析过程:
13 * 1. 获取http头部(首行+头部)
14 * 首行中包含url可以直到客户端请求什么资源,GET请求还可以获取到提交的数据
15 * 首行中包含的协议版本:拿到版本就可以针对不同版本``的特性进行处理
16 * 2. 解析头部
17 * 可以获取到正文有多长,正文是什么类型的数据
18 * 3. 获取正文进行处理(通常将正文交给子进程处理)
19 * 问题:如何获取头部--如何保证获取一个完整的头部
20 * *******************************************************/
21 #include <iostream>
22 #include <sstream>
23 #include "TcpSocket.hpp"
24
25 int main(int argc, char *argv[])
26 {
27 if (argc != 3) {
28 std::cout<< "./httpserver ip port\n";
29 return -1;
30 }
31 std::string ip = argv[1];
32 uint16_t port = atoi(argv[2]);
33
34 TcpSocket sock;
35 CHECK_RET(sock.Socket());
36 CHECK_RET(sock.Bind(ip, port));
37 CHECK_RET(sock.Listen());
38 while(1) {
39 TcpSocket clisock;
40 if (sock.Accept(clisock) == false) {
41 continue;
42 }
43 std::string buf;
44 clisock.Recv(buf);
45 std::cout << "req:["<< buf <<"]\n";
46
47 std::string body;
48 body = "<html><body><h1> Everything will be ok!</h1></body></html>";
49 std::stringstream ss;
50 ss << "HTTP/1.1 502 Bad GateWay\r\n";
51 ss << "Content-Length: " << body.size() <<"\r\n";
52 ss << "Content-Type: text/html\r\n";
53 ss << "Location: http://www.baidu.com\r\n";
54 ss << "\r\n";
55 std::string header = ss.str();
56
57 clisock.Send(header);
58 clisock.Send(body);
59 clisock.Close();
60 }
61 sock.Close();
62 return 0;
63 }
TcpSocket.hpp
| | 1 #include <stdio.h>
| | 2 #include <string>
| | 3 #include <string.h>
| | 4 #include <unistd.h>
| | 5 #include <sys/socket.h>
| | 6 #include <stdlib.h>
| | 7 #include <netinet/in.h>
| | 8 #include <arpa/inet.h>
| | 9 #include <errno.h>
| | 10 #include <iostream>
| | 11 //创建socket文件描述符(tcp/udp,客户端+服务端)
| | 12 // int socket(int domain, int type, int protocol);
| | 13 //建立连接 (TCP, 客户端)
| | 14 // int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| | 15 //绑定端口号 (TCP/UDP, 服务器)
| | 16 // int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
| | 17 // socklen_t address_len);
| | 18 //开始监听socket (TCP, 服务器)
| | 19 // int listen(int socket, int backlog);
| | 20 //建立连接 (TCP, 客户端)
| | 21 // int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| | 22 // 接收请求 (TCP, 服务器)
| | 23 // int accept(int socket, struct sockaddr* address, socklen_t* address_len);
| | 24 //名称 目的
| | 25 //AF_INET IPv4网络通信
| | 26 //AF_INET6 IPv6网络通信
| | 27 //AF_PACKET 链路层通信
| | 28 //AF_UNIX, AF_LOCAL 本地通信
| | 29
| | 30 //type
| | 31 //SOCK_ STREAM 字节流套接字
| | 32 //SOCK DGRAM 数据报套接字
| | 33 //SOCK_ .SEQPACKET 有序分组套接字
| | 34 //SOCK_ RAW 原始套接字
| | 35
| | 36 //protocol
| | 37 //IPPROTO_TCP TCP传输协议
| | 38 //IPPTOTO_UDP UDP传输协议
| | 39 //IPPROTO_SCTP STCP传输协议
| | 40 //IPPROTO_TIPCTCP TIPC传输协议
| | 41
| | 42 #define CHECK_RET(q) if((q) == false){return -1;}
| | 43
| | 44 typedef struct calculator_info_t {
| | 45 int num1;
| | 46 int num2;
| | 47 char str[30];
| | 48 char op;
| | 49 }calculator_info;
| | 50
| | 51 class TcpSocket
| | 52 {
| | 53 public:
| | 54 TcpSocket(): _sockfd(-1){
| | 55 }
| | 56 void SetSockFd(int fd){
| | 57 _sockfd = fd;
| | 58 }
| | 59 int GetSockFd(){
| | 60 return _sockfd;
| | 61 }
| | 62 bool Socket(){
| | 63 _sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
| | 64 if(_sockfd < 0){
| | 65 perror("socket errno\n");
| | 66 return false;
| | 67 }
| | 68 return true;
| | 69 }
| | 70 bool Bind(std::string &ip, uint16_t port){
| | 71 struct sockaddr_in addr;
| | 72 //struct sockaddr_in {
| | 73 //short int sin_family; //地址族,AF_xxx 在socket编程中只能是AF_IN
| | 74 //unsigned short int sin_port; // 端口号 (使用网络字节顺序)
| | 75 //struct in_addr sin_addr; //存储IP地址 4字节
| | 76 //unsigned char sin_zero[8]; // 总共8个字节,实际上没有什么用,只>
| | 77 //};
| | 78 addr.sin_family = AF_INET;
| | 79 //网络字节顺序与本地字节顺序之间的转换函数:
| | 80 // htonl()–“Host to Network Long”
| | 81 // ntohl()–“Network to Host Long”
| | 82 // htons()–“Host to Network Short”
| | 83 // ntohs()–“Network to Host Short”
| | 84 addr.sin_port = htons(port);
| | 85 addr.sin_addr.s_addr = inet_addr(ip.c_str());
| | 86 //数字的字节序转换: 主机字节序转换为网络字节序
| | 87 //4个字节的数据
| | 88 // uint32_t htonl(uint32_t hostlong);
| | 89 //2个字节的数据(不可用4字节转换)
| | 90 // uint16_t htons(uint16_t hostshort);
| | 91 //数字的字节序转换: 网络字节序转换为主机字节序
| | 92 // uint32_t ntohl(uint32_t netlong);
| | 93 // uint16_t ntohs(uint16_t netshort);
| | 94 //将点分十进制IP地址字符串转换为网络字节序IP地址
| | 95 // in_addr_t inet_addr(const char *cp);
| | 96 // int inet_pton(int af, const char *src, void *dst);
| | 97 //将网络字节序转换为字符串点分式十进制IP地址
| | 98 // const char *inet_ntop(int af, const void *src, char *dst, sockl
| | 99 // char *inet_ntoa(struct in_addr in);
| |100
| |101 socklen_t len = sizeof(struct sockaddr_in);
| |102 int ret = bind(_sockfd, (const struct sockaddr *)&addr,len);
| |103 if(ret < 0){
| |104 perror("bind errno\n");
| |105 return false;
| |106 }
| |107 return true;
| |108 }
| |109 bool Listen(int backlog = 10){
| |110 // int listen(int socket, int backlog);
| |111 //backlog:最大并发连接数--内核中已完成连接队列的最大节点数
| |112 int ret = listen(_sockfd,backlog);
| |113 if(ret < 0){
| |114 perror("listen errno\n");
| |115 return false;
| |116 }
| |117 return true;
| |118 }
| |119 bool Connect(std::string &ip,uint16_t port){
| |120 //int connect(int sock, const struct sockaddr *addr, socklen_t ad
| |121 //addr: 要连接的服务器地址信息
| |122 struct sockaddr_in addr;
| |123 addr.sin_family = AF_INET;
| |124 addr.sin_port = htons(port);
| |125 addr.sin_addr.s_addr = inet_addr(ip.c_str());
| |126 socklen_t len = sizeof(struct sockaddr_in);
| |127
| |128 int ret = connect(_sockfd,(struct sockaddr*)&addr, len);
| |129 if(ret < 0){
| |130 perror("connect errno\n");
| |131 return false;
| |132 }
| |133 else{
| |134 std:: cout<<"connect success\n";
| |135 return true;
| |136 }
| |137 }
| |138 bool Accept(TcpSocket &csock, struct sockaddr_in *addr = NULL){
| |139 //在一个套接字上接受一个连接
| |140 //int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
| |141 //addr: 客户端地址信息
| |142 //len: 输入输出参数,既要指定接受长度,还要接收实际长度
| |143 //返回值: 为新客户新建的socket套接字描述符
| |144 //通过这个返回的描述符可以与指定的客户端进行通信
| |145 struct sockaddr_in _addr;
| |146 socklen_t len = sizeof(struct sockaddr_in);
| |147 int newfd = accept(_sockfd,(struct sockaddr*)&_addr, &len);
| |148 if (newfd < 0){
| |149 perror("accept errno\n");
| |150 return false;
| |151 }
| |152 if (addr != NULL){
| |153 memcpy(addr, &_addr, len);
| |154 }
| |155 csock.SetSockFd(newfd);
| |156 //_socket--仅用于接受新客户端连接请求
| |157 //newfd--专门用于与客户端进行通信
| |158 return true;
| |159 }
| |160 bool Recv(std::string &buf){
| |161 char tmp[4096] = {0};
| |162 //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
| |163 //flags:0--默认阻塞接受 MSG_PEEK-获取数据但是不从缓冲区移除
| |164 //返回值:实际接受的数据长度 失败:-1 断开连接: 0
| |165 int ret = recv(_sockfd, tmp,4096,0);
| |166 if (ret < 0){
| |167 perror("recv error");
| |168 return false;
| |169 }
| |170 else if (ret == 0){
| |171 printf("peer shutdown\n");
| |172 return false;
| |173 }
| |174 buf.assign(tmp, ret);
| |175 return true;
| |176 }
| |177 bool Send(std::string &buf){
| |178 //int send(int s, const void *msg, size_t len, int flags);
| |179 int ret = send(_sockfd, buf.c_str(), buf.size(), 0);
| |180 if (ret < 0){
| |181 perror("send error");
| |182 return false;
| |183 }
| |184 return true;
| |185 }
| |186 bool Close(){
| |187 close(_sockfd);
| |188 _sockfd = -1;
| |189 }
| |190 // ~TcpSocket() {}
| |191 private:
| |192 int _sockfd;
| |193 };
传输层
端口号
在TCP/IP协议中,用"源IP"“目的IP”“目的端口号”“协议号”,这样一个五元组来标识一个通信(用netstat -n 查看)
范围划分
-
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的.
-
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.
-
一个进程是否可以bind多个端口号?
可以因为一个进程可以打开多个文件描述符,而每个文件描述符都对应一个端口号,所以一个进程可以绑定多个端口号 -
一个端口号是否可以被多个进程bind?
不可以如果进程先绑定一个端口号,然后在fork一个子进程,这样的话就可以是实现多个进程绑定一个端口号,但是两个不同的进程绑定同一个端口号是不可以的
netstat
查看网络状态 -
n 拒绝显示别名,能显示数字的全部转化成数字
-
l 仅列出有在 Listen (监听) 的服務状态
-
p 显示建立相关链接的程序名
-
t (tcp)仅显示tcp相关选项
-
u (udp)仅显示udp相关选项
-
a (all)显示所有选项,默认不显示LINSTEN相关
pidof
通过进程名,查看进程ID
UDP协议
UDP协议端格式
16位UDP长度,表示整个数据报(UDP首部 + UDP数据)的最大长度
如果校验和出错,就会直接丢弃
UDP的特点
无连接–知道对端的IP和端口直接进行传输,不需要建立连接
不可靠–没有确认机制,没有重传机制,如果因网络故障无法传输成功,也没有任何错误信息
面向数据报: 不能够灵活的控制读写数据的次数和数量
面向数据报
应用层的报文会按照原样发送,不会拆分,不会合并
如果发送端调用一次send,发送多少,接收端必须一次调用recv接收多少,不能分次循环调用
UDP的缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核直接将数据传给网络层协议进行后续的传输动作
UDP有接受缓冲区,但是这个接受缓冲区不能保证收到的UDP报和发送的UDP报顺序一致;如果缓冲区满了,在达到的UDP数据就会被丢弃.
UDP的socket既能读也能写,是为全双工通讯
UDP使用注意事项
UDP首部有一个16位的最大长度,意味着UDP一次传输最大长度64K(包含UDP首部)
一般来讲要在应用层进行手动分包,多次发送,在接收端手动拼装.
基于UDP的应用层协议 -
NFS: 网络文件系统
-
TFTP: 简单文件传输协议
-
DHCP: 动态主机配置协议
-
BOOTP: 启动协议(用于无盘设备启动)
-
DNS: 域名解析协议
TCP协议
TCP全称为 “传输控制协议(Transmission Control Protocol”).
TCP协议段格式
- 源/目的端口号: 表示数据从哪个进程来,到哪个进程去
- 32位序号:32位的序列号由接收端计算机使用,重新分段的报文成最初形式。当SYN出现,序列码实际上是初始序列码(Initial Sequence Number,ISN),而第一个数据字节是ISN+1。这个序列号(序列码)可用来补偿传输中的不一致。
- 32位确认序号:32位的序列号由接收端计算机使用,重组分段的报文成最初形式。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码。
- 4位TCP报文长度: 表示该TCP有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60
- 6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立即从TCP缓冲区把数据读走
RST: 对方要求重新建立连接,携带RST标识的称为复位报文段
SYN: 请求建立连接,带有SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段 - 16位窗口大小:用来表示想收到的每个TCP数据段的大小。TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个16字节字段,因而窗口大小最大为65535字节。
- 16位校验和:发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
- 16位紧急指针:标识哪部分数据是紧急数据;
三次握手四次挥手
三次握手:
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
四次挥手
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
可靠传输
确认应答(ACK)机制
协议字段中的序号/确认序号
因为TCP为了实现可靠传输牺牲了部分传输性能,并且可能因为ACK确认应答丢失也要重传数据.
每一个ACK都带有对应的确认序列号,即为告诉发送端,收到哪些数据,下一次从哪开始发
超时重传机制
前提: 每条数据的确认回复都必须按序回复,若前面的数据没有收到,则不会对后面的数据进行回复(乱序的情况)
1.意味着: 若收到一条回复,表示ack确认序号之前的数据全部安全到达,不会因为前面数据的ACK丢失而重传数据
2.若前面数据丢失,则接受党收到后发的数据,立即重发请求,并且连发三次,若发送方连续收到三条重传请求,则认为数据丢失,进行重传
连接管理机制
TIME_WAIT状态
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
- 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip,目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题.
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
滑动窗口
一次发送多条数据 - 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.
- 发送第一个窗口,不需要等待任何ACK, 直接发送;
- 收到第一个ACK后,滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区来记录当前还有哪些数据没有应答; 只有确 认应答过的数据, 才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高;
流量控制
为防止接收端缓冲区因处理数据速度有限而被打满,引入流量控制
TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数
据段, 使接收端把窗口大小告诉发送端.
注意 TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;为扩展16位窗口字段
拥塞控制
拥塞控制为了避免因为网络状况不好导致通讯初始,大量的数据报丢失重传,降低性能
慢启动, 快增长
发送端控制一个拥塞窗口大小, 在进行数据传输时候, 进行网络探测式的发送,若网络状况良好则发送的数据快速增长,达到阈值(窗口大小)时,则不再继续增长; 若传输过程中丢包,则重新初始化拥塞窗口
延迟应答
接收方收到数据后并不立即进行确认回复,而是等待一段时间; 因为这段时间内,有可能用户已经RECV将缓冲区中的数据取走,窗口就可以尽可能的保证最大大小;保证传输吞吐量
捎带应答(应用场景不太多)
接收方对每一条数据的确认回复,都需要发送一个TCP数据包; 但是空报头的传输会降低性能.
因此会考虑在即将要发送的数据包中包含有确认信息(可以少发一个确认的空报头)
捎带应带机制(产生场景不太多)
分区
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区和一个接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区; 然后应用程序可以调用read从接收缓冲区拿数据;
- TCP的一个连接,既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可 以写数据. 为全双工
粘包问题
包:应用层中的数据包
TCP中没有UDP中的"报文长度"字段,但是有序号字段
传输层: TCP是根据报文依次按照序号存入缓冲区
应用层: 只是一连串的字节数据
所以从应用层角度来说不知道数据包的开始结束位置.
解决粘包问题(明确两个包的边界)
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔
TCP异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
基于TCP应用层协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP