计算机网络传输层(超详细!)
传输层功能
将应用层的报文封装到传输层中,使用 TCP 或者 UDP 协议建立两个主机进程之间的端对端通信,
传输层会自动对上层数据进行分用和复用:不同的应用层报文可封装到同一个传输层报文中传输,到达后再拆分报文交付给应用层中的各个进程。通信双方根据套接字(IP 地址 + 端口号)相互识别。
UDP 协议
UDP 提供不可靠但高效的、无连接的服务:
-
在传送数据之前不需要建立连接,远地主机在收到 UDP 报文后也不需要给出任何确认(只检测报文检验和,出现错误直接丢弃)。
-
一般用于即时通信,如语音、视频、直播等场景。
TCP 协议
TCP 提供可靠的、面向连接的服务:
-
在传送数据之前必须先建立连接,数据传送结束后要释放连接。传输过程中会校验数据并返回确认,保证可靠性。
-
一般用于文件传输、发送和接收邮件、远程登录等场景。
TCP 连接
建立连接
建立 TCP 协议采用了三次握手策略:SYN 是 TCP/IP 建立连接时使用的握手信号,接收者到后以 ACK 信号响应(序号加一)。
- 客户端向服务端 发送带有 SYN 标志的数据包(客户端请求连接)
- 服务端向客户端 发送带有 SYN/ACK 标志的数据包(客户端可以正常发送消息)
- 客户端向服务端 发送带有带有 ACK 标志的数据包(服务端可以正常发送消息,连接建立)
断开连接
断开 TCP 连接采用了四次挥手策略:FIN 是 TCP/IP 断开连接时使用的终止信号,接收者到后以 ACK 信号响应(序号加一)。
- 客户端向服务端 发送带有 FIN 标志的数据包(客户端请求终止)
- 服务器向客户端 发送带有 ACK 标志的数据包(客户端不再发送消息,连接半关闭)
- 服务器向客户端 发送带有 FIN 标志的数据包(服务端请求终止)
- 客户端向服务端 发送带有 ACK 标志的数据包(服务端不再发送消息,连接关闭)
TCP 可靠传输
编号和校验和
- 编号
发送方将应用报文分割成 TCP 报文段后,会为每一个报文段进行编号。由接收方对数据包进行排序,再把有序数据传送给应用层。
如果收到重复报文段,直接丢弃。
- 校验和
发送方会计算 TCP 报文段全部数据的检验和,并保存在报文段首部。由接收方对数据包数据进行检验。
如果收到错误报文段,丢弃报文段,并返回错误消息等待重发。
IP 协议校验和只校验首部, TCP 协议校验全部数据。
ARQ 协议
(自动重传请求)发送方发送报文段后,会等待接收方 ACK。如果接收到错误消息或等待超时,将重发这个报文段。用来实现可靠传输。
现在一般使用连续 ARQ 协议:维持一个发送窗口,可以连续发送出去多个报文段而不需要等待对方确认。接收方对按序到达的最后一个报文段发送确认,即表明之前的所有报文段都已经正确收到。提高信道利用率。
如果发送方连续发送了 5 条消息,其中 3 号消息丢失。接收方只会对前两个消息发送确认。发送方必须对后三条消息全部重传。
流量控制和阻塞控制
-
流量控制:减少数据发送,防止接收方过载导致数据无法处理。
-
拥塞控制: 减少数据发送,防止网络阻塞导致数据无法到达。
发送方允许连续发送的分组上限受以上两个因素制约,即 发送窗口 = min (接收窗口, 阻塞窗口)
。
-
接收窗口大小由 接收方返回的确认报文中的窗口字段设定。
-
拥塞窗口大小根据网络的拥塞程度动态变化,TCP 拥塞窗口变化采取了慢开始、拥塞避免、快重传 和 快恢复 四种策略。
TCP通信
Socket类
Socket在建立网络连接时使用。连接成功时应用程序两端都会产生一个Socket实例指向对方,完成所需的会话。
服务器端
- 创建ServerSocket对象,绑定监听端口监听客户端请求。
- 接收客户端请求,创建Socket与该客户建立专线连接(多线程)。
- 双方通过输入输出流进行对话。
- 关闭流和套接字,继续等待新的连接。
客户端
- 创建Socket对象,指明需要连接的服务器地址和端口号。
- 连接建立后,通过输出流向服务器发送请求信息。
- 双方通过输入输出流进行对话。
- 关闭流和套接字。
创建Socket对象
ServerSocket server = new ServerSocket(55533);
创建ServerSocket对象,并指定服务器端接口
Socket socket = new Socket(127.0.0.1, 55533);
创建Socket对象,并指明对方主机和接口
监听端口
Socket socket = server.accept();
ServerSocket接收请求,并且返回一个客户端的Socket对象实例。
Accept方法用于产生“阻塞”(由循环产生),使程序运行停在这个地方,直到一个会话产生。
进行对话
//打开输出流
OutputStream out = socket.getOutputStream();
//输出信息(字节流)
String message = "Hello";
out.write(message.getBytes("UTF-8"));
out.write("end");
//关闭输出流
out.close();Copy to clipboardErrorCopied
输出流输出信息,另一端输入流将得到输入。失败则抛出IOException
//打开输入流(转化为字节流被缓冲流读取)
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//输入信息(接收到end字符则结束)
StringBuilder message = new StringBuilder();
while ((str = in.readLine()) != null && "end".equals(str)) {
message.append(str);
}
System.out.println("get message: " + message);
//关闭输入流
in.close(); //socket.shutdownOutput();Copy to clipboardErrorCopied
输入流输入信息,得到另一端输出流信息。失败则抛出IOException
//客户端
public class SocketClient {
public static void main(String args[]) throws Exception {
//与本地服务器端建立连接
String host = "127.0.0.1";
int port = 55533;
Socket socket = new Socket(host, port);
//控制台输入并向服务器端输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while (str != "end") {
String str = bufferedReader.readLine();
bufferedWriter.write(str + "\n");
bufferedWriter.flush();
}
//关闭连接
outputStream.close();
socket.close();
}
}Copy to clipboardErrorCopied
//服务器端
public class SocketServer {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);
//循环等待请求
while(true){
//建立连接
Socket socket = server.accept();
//从socket中获取输入流并读取
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String str;,
while ((str = bufferedReader.readLine()) != null) {
System.out.println("get message from client:" + str);
}
//关闭连接
inputStream.close();
socket.close();
}
}
}Copy to clipboardErrorCopied
1> 多线程
每有一个Socket请求的时候,就创建一个线程来处理它。(通常交给线程池管理,保证线程的复用)
能够循环处理多个Socket请求,否则一个请求的处理耗时,后面的请求将被阻塞。
//服务器端(线程池管理)
public class SocketServer {
public static void main(String[] args) throws IOException {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
//循环等待请求
while (true) {
//建立连接
Socket socket = serverSocket.accept();
//分配线程
Runnable runnable = () -> {
BufferedReader bufferedReader = null;
//从socket中获取输入流并读取
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println("get message from client:" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
};
executorService.submit(runnable);
}
}
}Copy to clipboardErrorCopied
2> 规范流输入输出长度
在实际应用中,socket发送的数据并不是按行发送。我们就不会每次发送数据,都用“\n”标识区分是否发送完毕。
在实际应用中,我们通过是采用数据长度+类型+数据的方式来告知一次流输入完成,方便进行后续操作。
https://blog.csdn.net/qq_33865313/article/details/79363640
https://www.cnblogs.com/huanzi-qch/p/9889521.html
Socket的消息推送机制中,用的都是 Ajax 轮询。在特定的时间间隔由客户端自动发出请求,将服务器的消息主动拉回来(服务器启动一个线程去监听与此客户端的通信),这种方式是非常消耗资源的,因为它本质还是http请求,而且显得非常笨拙:服务端不能主动向客户端推送数据。
####WebSocket类
import javax.websocket.*;
WebSocket 在浏览器和服务器完成一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。多客户端、涉及有界面的聊天建议使用websocket(嵌入到了浏览器的内核中)。
- 点赞
- 收藏
- 关注作者
评论(0)