传输层
端口号:0~65535 但是1~1023基本被占用了,而且占用的程序好像活着的不多了,
用的话基本就是从1024开始往后排。
常用的端口号:ssh:22 (Liunx的远程连接)
http:80
https:443
ftp:21(文件传输协议)
telnet:23(明文远程连接)
DNS:53
常问的:一个进程可以bind多个端口号(一个进程里面是有好多个线程,每一个线程bind一个 端口号是可以的)。
一个端口号是不可以被多个进程bind的。
五元组:源ip 目的ip 源端口 目的端口 协议类型(tcp/udp)
UDP(无连接,不可靠传输,面向数据报,全双工):
┌─────────┬──────────┬─────────┬────────────┐
│ 16位源端口 │16位目的端口 │ 16位长度 │ 16位校验和 │ ← 首部 8 字节
├─────────┴──────────┴─────────┴────────────┤
│ 数据(payload) │
└────────────────── ─────────────────────────┘
16位长度:就是传输的数据的大小 16位bit—>64kb (可以传的东西大小非常小)
如果需要传输大的数据(大于64kb)只能分多次去发送,去拆包,再组包。
16位校验和:数学公式验证这个传输前后的数据有没有受损坏。
UDP 报头中的校验和是个 2 字节的数据
使用于:不容易丢包的地方,对传输效率高的地方,传小的数据的地方。
代码实现:服务端:UdpEchoServer
package Socket; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UdpEchoServer { // 服务端使用的 DatagramSocket 对象 private DatagramSocket socket = null; // 构造方法:端口号 public UdpEchoServer(int prot) throws SocketException { socket = new DatagramSocket(prot); } //启动服务端 //处理客户端请求 public void start() throws IOException { System.out.println("Server start"); while (true){ //作为服务器先接收信息 DatagramPacket requsetPacket = new DatagramPacket(new byte[1024] , 1024); socket.receive(requsetPacket); //得到的数据报转化为string类型的 String request = new String(requsetPacket.getData() ,0, requsetPacket.getLength()); //在服务端去处理这个数据 String response = possen(request); //重新封装返回的数据报 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length, requsetPacket.getSocketAddress()); //发送数据报 socket.send(responsePacket); //打印日志 System.out.printf("[%s:%d] res:%s rep:%s \n" , requsetPacket.getAddress().toString(),requsetPacket.getPort(), request,response); } } //处理业务逻辑 public String possen(String request) { return request; } //主方法 public static void main(String[] args) throws IOException { UdpEchoServer udpEchoServer = new UdpEchoServer(9999); udpEchoServer.start(); } }客户端:UdpEchoClient
package Socket; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Scanner; public class UdpEchoClient { // 服务端使用的 DatagramSocket 对象 private DatagramSocket socket = null; // 服务器端口 private int serverPort; // 服务器ip private String serverIp; // 构造方法 创建客户端 需要服务器的端口和ip public UdpEchoClient(String serverIp ,int serverPort ) throws SocketException { socket = new DatagramSocket(); this.serverPort = serverPort; this.serverIp = serverIp; } // 启动客户端的 public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("client start"); while (true){ //作为客户端需要先发送 System.out.printf("->"); //读取用户输入 String text = scanner.nextLine(); //封装成数据包发送 需要服务器的ip和端口 DatagramPacket response = new DatagramPacket(text.getBytes(),text.getBytes().length, InetAddress.getByName(this.serverIp),this.serverPort); //发送数据包 socket.send(response); //接收服务器回显的数据包 需要一个数据包来接收 DatagramPacket resquestPath = new DatagramPacket(new byte[1024] , 1024); socket.receive(resquestPath); //将数据包中的数据转换成字符串 String ressquest = new String(resquestPath.getData(),0, resquestPath.getLength()); //打印输出 System.out.println(ressquest); } } // 程序的入口的 public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9999); udpEchoClient.start(); } }TCP(有连接,可靠传输,面向字节流,全双工):
0 15 16 31 +------------------------+------------------------+ | 16位源端口号 | 16位目的端口号 | 第1行 +------------------------+------------------------+ | 32位序号 (seq) | 第2行 +---------------------------------------------------+ | 32位确认序号 (ack) | 第3行 +--------+-------+---+---+---+---+---+---+----------+ |4位首部 |保留(6)|URG|ACK|PSH|RST|SYN|FIN|16位窗口 | 第4行 | 长度 | | | | | | | | 大小 | +--------+-------+---+---+---+---+---+---+----------+ | 16位检验和 | 16位紧急指针 | 第5行 +------------------------+------------------------+ | 选项(长度可变,0~40字节) | 第6行 +---------------------------------------------------+ | 数据 (payload) | 第7行 +---------------------------------------------------+
16位检验和:和UDP一样
4位报头长度:4bit范围是0~15 也就是0~4*15=60(必须的4的倍数啊)
6位标识符:ack(确认报文) ret(复位报文) syn(同步报文) fin(结束报文)
基于可靠性的特性
1.确认应答
每次的发送数据,都会收到一个ack,也会携带下一个的起始点。
2.超时重传 ---> 可靠传输
丢包:数据丢了,ack丢了
重新传报:都是因为没有传回的ack而需要重传的,
超时时间指数递增,累计到一定次数 → 认为网络/对端异常,强制关闭连接
如果是数据丢了,重新传,无事发生
如果是ack丢了,重新传数据,会重复,是根据传数据的序列号去重
3.连接管理
连接的时候(三次握手):也可以是四次握手 发ack 和 syn 分开发
listen:准备就绪
establisten:连接完毕(可以通信了)
syn_sent:发送连接(很快)
syn_rcvd:接收连接(很快)
断开连接的时候(四次挥手):服务器应用层是否还有未发送的数据决定了是不是可以 合并SYN和ACK
FIN_WAIT_1:主动关闭方,发出的FIN后,还没有收到ACK的状态。
FIN_WAIT_2:主动关闭方,收到ACK,还没有收到FIN的状态
TIME_WAIT:主动关闭方收到FIN的状态,等待 2MSL 后进入 CLOSED
CLOSE_WAIT:被动关闭方收到FIN的进入还没有调用CLOSE()的方法状态。
LAST_ACK:已发FIN,等最后ACK
CLOSED:关闭连接的状态。
4.滑动窗口
每次发一个数据,就需要恢复一次ACK,影响效率会变慢,可以一组一组发,ACK返回下 一个数据的序号。
会出现的问题:
ACK丢失:
虽然传输期间有一部分ACK丢失,但是由于ACK返回的都是下一个的序号,后面最后的 ACK如果是正常的,那就是其实证明了数据的传输没有问题
数据丢失:
数据丢失,ACK返回的一直都是确实的的序号。(问题不大)
处理数据的时候,都是先进入接收方的数据缓冲区,当需要不不连续的时 候,后面的数 据就先放到缓存器等待,等正确需要数据序号来了再继续处理,排序去重 也是都是在数 据缓冲区里面处理的
快重传:发送端连续收到几次相同的ACK(如好几次ACK=1001),立刻重传丢失的数 据,不用等超时。
补齐后,回复的是总的数据的下一个序号。
5.流量控制
16位窗口大小里面的就是ACK也返回的是数据缓存器剩余的空间。
问题:如果此时的缓存器里面的空间为0了,就不发了,那发送方咋知道啥时候有空间。
A会每隔一段时间发送一个空的报文(窗口探测)
{去“敲门”问 B:“哥们,腾出空间了吗?现在能收多少?”}。
发给B,B如果有空间就回复空间 了,如果还是没有B还是返回0.
6.拥塞控制
传输数据嘛,一下子太大的话会导致数据丢失。
先设定一个阈值,最开始从1份开始 ----->份不是固定值 是自己设置的
纵坐标嘛就是阻塞窗口, 实际发送量 =min(拥塞窗口, 接收窗口)
到了阈值之前,发送的数据大小都是以指数增长。
到了阈值,还没有阻塞的话,后面开始匀速增长。
如果期间数据缓存器产生了阻塞,开始缩小窗口,设定新的阈值为产生阻塞位置一半
老办法:直接回到1份,继续上述的过程
新办法:到达新的阈值的位置开始匀速增长
7.延迟应答
ACK不是看到一组就返回一次,而且延迟回复,让数据多传几次,再去返回
可以提高效率,减少了ACK报的数量
延迟 时间后,窗口变大了,发送方就能发更多,吞吐量就上去了
限制:不能死等,受“每2个包回一次”和“最大延迟200ms”的限制。
8.捎带应答
类似建立连接的时候三次握手,第二次是就是服务端返回了syn和ack 就是捎带应答。
每次回答的时候,如果还是别的数据可以顺带一个发送。
9.面向字节流--->粘包问题
TCP是面向字节流的,不像UDP是有自己的DatagramPacket
以至于发送的时候会边界丢失,不知道那些字节组合是一个完整的数据
解决:分隔符:去限制当输入到什么时候就判定为输入完毕,就是一组数据
处理的时候也是当出现这个符号的时候前面就是完整的数据。
(比如\n或者\r\n)作为边界
数据开头属性:在每个发的数据前面加上字符的长度,处理就按长度处理。
10.异常处理
1.进程奔溃
退出的状态就是和正常的四次挥手状态一样。
虽然是进程没有了,但是连接还是在的。
2.主机正常关机
强制结束所有的进程(电脑关机会显示有一个正在阻止关机,下面的强制关 机)
正常的四次挥手关闭是为了让双方都删除对方的信息,服务端发好多了FIN后
发现一直都没有回应,那就算了,直接单方面断开,删除客户端的连接信息。
3.主机断电关机
当接收方的直接断电跑路,此时发送方发FIN或者别的什么都不会有回应的,
无论多少次都是超时(红色感叹号)就需要放弃了,
最后再发RST(复位报文)就算放弃了(都是陌生人了)。
4.网线断开
属于是双方无法正常通信, 但是双方不知道是什么原因(陌生人了还是有事 了)
发送方:会发一个”心跳包“ -->什么都没有只为了让对方返回ACK
如果没有回复,就断开连接删除信息(不理我算了,删了)
代码实现:服务端:TcpEchoServer
package Socket; import jdk.jshell.spi.ExecutionControl; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TcpEchoServer { // 服务端使用的 ServerSocket 对象 private ServerSocket serverSocket = null; //构造方法需要服务器端口 public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("TCPserver.start"); // ExecutorService cached = Executors.newCachedThreadPool(); while (true){ //把已经连接的客服端弄成socker Socket socket = serverSocket.accept(); //进入这个方法 cached.submit(() -> processConnection(socket)); //也可以用多线程来运行 // Thread thread = new Thread(() -> { // processConnection(socket); // }); } } private void processConnection(Socket socket) { System.out.printf("[%s:%d]上线了\n" ,socket.getInetAddress(),socket.getPort()); try { //负责读写的 InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); //负责发送和接收的 Scanner scanner = new Scanner(inputStream); PrintWriter writer = new PrintWriter(outputStream); while (true){ if (!scanner.hasNext()){ System.out.printf("[%s:%d]下线了\n" ,socket.getInetAddress(),socket.getPort()); break; } //接收的东西 String request = scanner.next(); //处理一下 准备回显回去 String response = process(request); //发回去从服务器到客户端 writer.println(response); // 刷新缓冲区 确保数据被发送出去 writer.flush(); System.out.printf("[%s:%d] req:%s res:%s\n" , socket.getInetAddress(),socket.getPort(),request,response); } } catch (IOException e) { throw new RuntimeException(e); } finally { try { // 关闭连接 socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } public String process(String request ) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer = new TcpEchoServer(9999); tcpEchoServer.start(); } }客户端:TcpEchoClient
package Socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; import java.util.ServiceConfigurationError; public class TcpEchoClient { // 客户端使用的 Socket 对象 private Socket socket = null; //构造方法 需要服务器的ip和端口 public TcpEchoClient(String serverIp , int serverPort) throws IOException { socket = new Socket(serverIp , serverPort); } public void start(){ System.out.println("client start!"); try{ //负责读写的 InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); //负责处理读写的 Scanner scannerRead = new Scanner(inputStream); PrintWriter writer = new PrintWriter(outputStream); //负责输入的 Scanner scanner = new Scanner(System.in); while (true){ System.out.printf("->"); String request = scanner.next(); //发给服务器的东西 writer.println(request); writer.flush(); //接收回显 if (!scannerRead.hasNext()){ break; } //读回显打印 String response = scannerRead.next(); System.out.println(response); } }catch (IOException E){ E.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9999); tcpEchoClient.start(); } }