网络编程套接字
UDP数据报套接字
DatagramSocket API
DatagramPacket API
InetSocketAddress API
利用上述知识实现一个回显服务器和客户端:
服务器代码:
public class UDPEchoServer {
//网络编程,本质上是要操作网卡
//但是网卡不适合直接操作,因此在操作系统内部,抽象了一个“socket”这样的文件夹来表示网卡
//因此进行网络通信,必须要有一个socket对象
private DatagramSocket socket = null;
//对于服务器来说,创建socket对象时,一定要给绑定一个具体的端口号
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
//服务器不是给一个客户服务,而是给一群客户服务
while(true){
System.out.println("服务器启动");
//1. 读取客户端发送过来的请求是什么
// receive方法的参数是一个输出型参数,需要先构建一个空白的DatagramPacket对象,交给receive来填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
// 此时这个 DatagramPacket 是一个特殊的对象, 并不方便直接进行处理. 可以把这里包含的数据拿出来, 构造成一个字符串.
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2. 根据请求计算相应
String response = process(request);
//3.把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好.
// 此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4. 打印当前请求的处理结果
System.out.printf("[%s,%d] res: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//port的取值范围为1024-65535
UDPEchoServer udpEchoServer = new UDPEchoServer(9999);
udpEchoServer.start();
}
}
客户端代码:
public class UDPEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
public UDPEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
while(true){
//1. 从控制台读取要发送的信息
Scanner scanner = new Scanner(System.in);
System.out.print(">");
String request = scanner.nextLine();
if(request.equals("exit")){
System.out.println("goodbye!");
break;
}
//2. 构造UDP请求并发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.接受响应并解析
DatagramPacket responsePacket = new DatagramPacket( new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.显示响应结果
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1",9999);
udpEchoClient.start();
}
}
TCP流套接字编程
ServerSocket API
ServerSocket是创建TCP服务端Socket的API
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端的 Socket。
基于TCP流套接字实现一个回显服务器:
服务器:
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//工作流程
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();
while(true){
//使用clientSocket和具体的用户进行通信
Socket clientSocket = serverSocket.accept();
//此处如果不使用多线程,那么当程序进入processConnection方法后,就会在循环中一只执行,知道当前客户端退出连接
//也就是说不使用多线程时,服务器只能对一个客户端进行服务,除非当前连接退出后才能向其他客户端进行服务
pool.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述socket对象和客户端进行通信
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//由于要处理多个请求,因此用循环来处理
while(true){
//1.读取请求
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//没有下一个数据,说明客户端关闭了连接
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求构造响应
String response = process(request);
//3.返回响应
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//保证数据进行了刷新
printWriter.flush();
System.out.printf("[%s:%d] res:%s, resp:%s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}catch(IOException e){
e.printStackTrace();
}finally {
//保证关闭能够执行到
clientSocket.close();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端:
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
//socket构造方法能够识别点分十进制的ip地址,比DatagramPacket方便。
socket = new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("客户端启动!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true){
//1.从控制台读取请求
Scanner scanner = new Scanner(System.in);
System.out.print(">");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodbye!");
break;
}
//2.构造请求并发送
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3.对响应进行解析
Scanner scanner1 = new Scanner(inputStream);
String response = scanner1.next();
//4.将响应显示在控制台
System.out.println(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
运行截图:
网络原理知识:
UDP协议
UDP的特点:无连接,不可靠,面向数据报,全双工通信,大小受限
UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。
TCP协议
4位首部长度:TCP报头长度可变,该部分描述了TCP报头的具体长度,其单位是4个字节(0-15)。
TCP报头固定部分长度为20字节,可变部分即选项部分。
TCP的序号和确认号:
32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。
32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。
TCP的标志位
每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。
用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。
TCP内部的工作机制(重点)
1)确认应答机制(安全机制)
2)超时重传机制
主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了:
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
具体步骤如下:
- 当发送方发送一个TCP报文段后,会启动一个计时器(即超时计时器);
- 发送方会等待一段时间(即重传时间),等待接收方回复收到报文段的确认段。如果在这段时间内,发送方未收到确认段,那么就说明这个报文段可能由于网络原因而丢失了;
- 当超时时间到期时,发送方就会重新发送之前发送的那个TCP报文段;
- 如果重传次数达到了一个预设值,发送方就不再发送该报文段,并向上层协议发送Time Out(超时)错误通知;
- 如果接收方收到了已经超时重传的报文段,它就会返回一个之前已经发送过的确认段,这个确认段表示接收方已经成功接收到这个报文段。 需要注意的是,TCP的超时重传机制通常使用指数退避算法来计算重传时间。即当一次重传失败时,重传时间会指数级增加,以避免网络拥塞。
3)连接管理机制
TCP 三次握手建立连接
-
第一次握手:
客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。 -
第二次握手:
服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。 -
第三次握手:
客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
注意:我们上面写的ack和ACK,不是同一个概念:
- 小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,ack=seq+1。
- 大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1。
三次握手的意义:
- 让通信双方各自建立对对方的“认同”;
- 验证通信双方的发送能力和接收能力;
- 在握手的过程中,双方协商一些重要的参数,数据同步等。
TCP断开连接:四次挥手
为何是四次挥手?
TCP是全双工通信,client向server发送FIN,表示client无数据向server发送,server回复ACK确认后只表示从client -> server 方向的数据流无数据可发,但是server -> client 方向的数据可能还没发送结束,需要继续发送,待服务器端数据也发送结束后,server向client发送FIN,client收到后发送ACK确认断开连接,此时server断开连接,client需要等待2MSL后断开连接。
编程角度解释:
为何要等到2MSL的时间?
1.保证TCP协议的全双工连接能够可靠关闭,即防止最后一次挥手时的ACK丢失,能够超时重传。
2.保证这次连接的重复数据彻底从网络中消失。
MSL:Maximum Segment Lifetime(最大分段生存时间),默认值是60S.
4)滑动窗口
滑动窗口的本质就是不等待的批量发送一堆数据,然后使用一份时间来等待一组数据的ack.
把不需要等待就能直接发送的数据量的大小,称为“窗口大小”,图示中的窗口大小为4000.
如果出现了丢包,如何进行重传?
情况二:数据包直接丢了
当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001" 一样;
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 -2000 重新发送;
这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为快重传。
5)流量控制
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发 送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制;
6)拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。
TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
此处引入一个概念程为拥塞窗口
发送开始的时候,定义拥塞窗口大小为1;
每次收到一个ACK应答,拥塞窗口加1;
每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口;
开始增加时是指数增加,等达到某一阈值时开始每次+1,遇到网络拥堵重新从1开始。
7)延时应答
在接收方能够处理的情况下,延时发送确认ack,使得滑动窗口尽可能的大一些。
在延时应答机制下,不会对每一条数据都返回一个ack,而是有一定间隔才返回。
8)捎带应答
在延时应答的基础上,aCK报文携带一定的业务数据,较少数据传输次数。
9)面向字节流
粘包问题:TCP发送的数据是数据流,应用层程序无法区分包和包之间的界限,每次拿到的数据可能是一个完整的包,也可能是不完整的。
解决办法:通过分隔符或者约定好包的长度,从而用于区分包和包之间的界限。
10)异常情况
进程崩溃/主机关机(主动关机):此时相当于socket.close(),内核会继续完成四次挥手,是一个正常的断开流程。
主机掉电/网线断开:如果是接收方故障,发送方会超时重传,一直等不到确认后断开连接;如果是发送方故障,接收方会通过心跳包来确认发送方的状态,如果一直没有回应就断开连接。
TCP 和 UDP 的对比:
TCP可靠传输,效率一般;
UDP效率很高,不可靠,天然支持广播;
KCP基于两者之间。
网络层协议
主要功能:地址管理,路由选择
IP协议介绍:
4位版本号(version):指定IP协议的版本,对于IPv4来说,就是4。
4位头部长度(header length):IP头部的长度是多少个32bit,也就是 length * 4 的字节数。4bit表示最大的数字是15,因此IP头部最大长度是60字节。
8位服务类型(Type Of Service):3位优先权字段(已经弃用),4位TOS字段,和1位保留
字段(必须置为0)。4位TOS分别表示:最小延时,最大吞吐量,最高可靠性,最小成本。
这四者相互冲突,只能选择一个。对于ssh/telnet这样的应用程序,最小延时比较重要;对于ftp这样的程序,最大吞吐量比较重要。
16位总长度(total length):IP数据报整体占多少个字节。
16位标识(id):唯一的标识主机发送的报文。如果IP报文在数据链路层被分片了,那么每一个片里面的这个id都是相同的。
3位标志字段:第一位保留(保留的意思是现在不用,但是还没想好说不定以后要用到)。第二位置为1表示禁止分片,这时候如果报文长度超过MTU,IP模块就会丢弃报文。第三位表示"更多分片",如果分片了的话,最后一个分片置为1,其他是0。类似于一个结束标记。
RFC标准定义以太网的默认MTU值为1500。
13位分片偏移(framegament offset):是分片相对于原始IP报文开始处的偏移。其实就是在表示当前分片在原报文中处在哪个位置。实际偏移的字节数是这个值 * 8 得到的。因此,除了最后一个报文之外,其他报文的长度必须是8的整数倍(否则报文就不连续了)。
8位生存时间(Time To Live,TTL):数据报到达目的地的最大报文跳数。一般是64。每次经过一个路由,TTL -= 1,一直减到0还没到达,那么就丢弃了。这个字段主要是用来防止出现路由循环。
8位协议:表示上层协议的类型。
16位头部校验和:使用CRC进行校验,来鉴别头部是否损坏。
32位源地址和32位目标地址:表示发送端和接收端。
解决IP地址不够用的方法:
1)动态分配IP地址
2)NAT网络地址转换
内网:10.* 172.16.* - 172.31.* 192.168.* 公网:其余ip
3)IPV6 128位,从根本上解决
数据链路层
考虑相邻两个节点之间的传输。
以太网数据帧:
帧协议类型字段有三种值,分别对应IP、ARP、RARP;
ARP:地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
RARP:逆地址解析协议,和上述协议功能相反。