AI摘要:本文介绍了网络编程中的UDP和TCP协议,包括它们的API、代码示例和网络原理。UDP是一种无连接、不可靠、面向数据报的传输层协议,适用于不需要建立连接的场景。TCP则是一种面向字节流、可靠的传输层协议,适用于需要稳定传输的场景。文章通过echo回显服务器和翻译服务器的示例代码,展示了UDP和TCP在实际应用中的使用。同时,还介绍了UDP报文结构和特点,以及基于UDP的应用层协议。
Powered by AISummary.
网络编程
一.UDP
1.1 api
1.1.1 DatagramSocket
DatagramSocket是一个Scocket对象。那么Socket又是什么呢?
我们的操作系统中,往往使用文件这样的概念来管理一些软硬件资源。我们这里的Socket文件,就是用来管理网卡的一种文件。
Java中的Socket对象,就对应了系统里的Socket文件,要进行网络通信,必须得先有Socket对象。
♾️ java 代码:DatagramSocket()
DatagramSocket(int port)
DatagramSocket有带端口和不带端口的构造方法,那么我们应该如何选择呢?
当在客户端使用时,我们往往让系统来自动分配,而在服务端时,我们往往手动指定。
1.1.2 DatagramPacket
DatagramPacket表示了一个UDP数据报,代表了系统中设定的UDP数据报的二进制结构。
UDP是面向数据报的,每次进行传输,都要以UDP数据报为基本单位。
下面是DatagramPacket的几个构造方法,DatagramPacket作为UDP数据报,必然要承载一部分的数据,这就需要我们通过手动指定的byte[]作为数据存储的空间
♾️ java 代码:public DatagramPacket(byte[] buf, int length,InetAddress address, int port)
public DatagramPacket(byte[] buf, int length)
....
1.2 代码示例
1.2.1echo回显服务器
♾️ java 代码:package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//创建一个DatagramScocket对象
//这是后续操作网卡的基础
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
/*
我们在服务器和客户端中均需要创建一个Socket对象
但是服务器的socket一般需要显示的指定一个端口号
而客户端的socket一般不能显示指定(不显示指定时候系统会自动分配一个随机的端口)
*/
}
//通过这个方法开启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//我们在使用socket.receive来接受数据时,需要先创建一个数据报(DatagramPacket)用来接受从网卡读取到的数据,
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//1.读取请求并解析
socket.receive(requestPacket);
//当前完成recive之后,数据以二进制的形式存储到DatagramPacket中
//要想能够把数据显示出来,还需要把二进制转成字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.由于我们此处是回显服务器,所以请求=响应。
String reponse = process(request);
//3.把响应写回客户端
// 搞一个响应对象,DatagramPacket
// 往DatagramPacket中填入刚才的数据,在通过sent返回
//传入数据报,数据报长度,和请求中的地址
DatagramPacket reponsePacket = new DatagramPacket(reponse.getBytes(),reponse.getBytes().length,requestPacket.getSocketAddress());
//注意这个长度不可以更改成reponse.length,因为如果这个字符串的内容都是英文字符,此时字节和字符个数是一样的,但是如果包含中文就不一样了。
socket.send(reponsePacket);
//4.打印日志
System.out.printf("[%s:%d] req=%s res=%s\n",reponsePacket.getAddress().toString(),reponsePacket.getPort(),request,reponse);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIp = "";
private int severPort = 0;
public UdpEchoClient(String severIp,int severPort) throws SocketException {
//创建这个对象不能手动指定端口
socket = new DatagramSocket();
//由于UDP自身不回持有对端的信息,就需要在应用程序中把对端的情况记录下来。
this.severIp = severIp;
this.severPort = severPort;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner sc = new Scanner(System.in);
while(true){
//1.从控制台读取一个数据,作为请求
System.out.print("->");
String request = sc.next();
//2.把请求内容构造成一个DatagramPacket对象,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName((severIp)), severPort);
socket.send(requestPacket);
//3.尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//4.把响应转换成字符串,并打印输出
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
1.2.2 翻译服务器
♾️ java 代码:package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("dog", "小狗");
dict.put("cat", "小猫");
}
@Override
public String process(String request) {
return dict.get(request);
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
1.2.3 总结
1.服务器先启动,服务器启动之后,就会进入循环,找到receive并阻塞(当客户端还未传入数据时)
2.客户端启动,也会先进入while循环,执行sc.next,并且也在这里阻塞
当用户在控制台输入字符串之后,next就会返回,从而构造请求数据并发送给服务器。
3.客户端发送出数据之后
服务器:就会从receive中返回,进一步的执行解析请求为字符串,执行process操作,执行send操作
客户端:继续往下执行,执行到receive等待服务器的响应、
4.客户端收到从服务器返回的数据后,就会从receive中返回
执行到这里的打印操作,接着显示出响应内容
5.服务器完成一次循环之后,又会执行到reveive
客户端完成一次循环之后,又会执行到sc.next
双双进入阻塞等待
2.TCP
2.1 API
TCP的socketapi和UDP的socket api差异很大,接下来让我们往下看
2.1.1 ServerSocket
给服务器使用的类,用来绑定端口号
2.1.2 Socket
既会给服务器用,又会给客户端使用
2.1.3 对比
- TPC是有连接的 UDP是无连接
- TCP是可靠传输 UDP是不可靠传输
- TCP是面向字节流 UDP是面向数据报
- TCP和 UDP都是全双工
连接:通信双方是否会记录保存对端的信息。
UDP:每次发送数据都需要手动在send方法中指定
TCP:连接如何建立,不需要代码干预,由系统内核自动完成
对应用程序来说:
- 客户端主要是发起“建立连接”的动作
- 服务器主要是把建立好的连接从内核中拿到应用程序里
如果有客户端和服务器要建立连接,这时服务器的应用程序是没有任何感知的,内核直接就完成了建立连接的流程(三次握手),完成流程之后,就会在内核的队列中(每一个ServerSocket都有这样的一个队列)排队。
应用程序要想和客户端进行通信,就需要通过一个accept方法,把内核中已经建立好的连接对象,拿到应用程序中
2.2代码示例
2.2.1代码
♾️ java 代码:package network;
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 {
private ServerSocket severSocket = null;
public TcpEchoServer(int port) throws IOException {
severSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService service = Executors.newCachedThreadPool();
while(true){
//通过accetp方法,把内核中已经建立 好的链接拿到应用中
//建立连接的细节流程是内核自动完成的,应用程序只需要捡现成的即可
Socket clientSocket = severSocket.accept();
service.submit(new Runnable(){
@Override
public void run(){
processConnection(clientSocket);
}
});
}
}
//通过这个方法来处理当前的连接
public void processConnection(Socket clientSocket){
//进入方法,打印出一个日志,表示当前有客户端连接上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 使用 try ( ) 方式, 避免后续用完了流对象, 忘记关闭.
// 由于客户端发来的数据, 可能是 "多条数据", 针对多条数据, 就循环的处理.
while(true){
Scanner sc = new Scanner(inputStream);
if(!sc.hasNext()){
// 连接断开了. 此时循环就应该结束
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 1. 读取请求并解析. 此处就以 next 来作为读取请求的方式. next 的规则是, 读到 "空白符" 就返回.
String request = sc.next();
// 2. 根据请求, 计算响应.
String response = processs(request);
// 3. 把响应写回到客户端.
// 可以把 String 转成字节数组, 写入到 OutputStream
// 也可以使用 PrintWriter 把 OutputStream 包裹一下, 来写入字符串.
PrintWriter printWriter = new PrintWriter(outputStream);
// 此处的 println 不是打印到控制台了, 而是写入到 outputStream 对应的流对象中, 也就是写入到 clientSocket 里面.
// 自然这个数据也就通过网络发送出去了. (发给当前这个连接的另外一端)
// 此处使用 println 带有 \n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据.
printWriter.println(response);
// 此处还要记得有个操作, 刷新缓冲区. 如果没有刷新操作, 可能数据仍然是在内存中, 没有被写入网卡.
printWriter.flush();
// 4. 打印一下这次请求交互过程的内容
System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally{
try {
// 在这个地方, 进行 clientSocket 的关闭.
// processConnection 就是在处理一个连接. 这个方法执行完毕, 这个连接也就处理完了.
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String processs(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
♾️ java 代码:package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int ServerPort) throws IOException {
// 需要在创建 Socket 的同时, 和服务器 "建立连接", 此时就得告诉 Socket 服务器在哪里~~
// 具体建立连接的细节, 不需要咱们代码手动干预. 是内核自动负责的.
// 当我们 new 这个对象的时候, 操作系统内核, 就开始进行 三次握手 具体细节, 完成建立连接的过程了.
socket = new Socket(serverIp,ServerPort);
}
public void start(){
Scanner sc = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter writer = new PrintWriter(outputStream);
Scanner scannerNetWork = new Scanner(inputStream);
while(true){
//1.从控制台读取用户输入的内容
System.out.println("->");
String request = sc.next();
// 2. 把字符串作为请求, 发送给服务器
// 这里使用 println, 是为了让请求后面带上换行.
// 也就是和服务器读取请求, scanner.next 呼应
writer.println(request);
writer.flush();
//3.读取服务器返回的相应
String response = scannerNetWork.next();
//4.输出内容
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",9090);
tcpEchoClient.start();
}
}
2.2.2 注意事项
clientSocket需要进行close操作
♾️ java 代码:Socket clientSocket
在UDP中的DatagramSocket和ServerSocket都没有写close,那为什么没问题呢?
因为UDP中的DatagramSocket和ServerSocket的周期是贯穿着整个程序的,只有这么一个对象,所以并不会频繁的创建,因此不会造成内存泄漏问题。
但是TCP中,ClientSocket是在一个循环中,每当有新的客户端来建立连接就会创建出一个新的ClienbtSocket
虽然我们已经在下面进行了trycatch操作,但这里关闭的知识clinetSocker上自带的流对象,并没有关闭socket本身
![image-20231022121135160](https://image.33mc.cn/luntan/202310221211056.webp)
所以,我们需要在代码中,通过finally加上close,来确保socket能被正确的关闭
![image-20231022121910772](https://image.33mc.cn/luntan/202310221219161.webp)
使用线程池来建立多端连接
为什么我们需要线程池来处理多端连接问题呢?
![image-20231022130700601](https://image.33mc.cn/luntan/202310221320012.webp)
如图所示,图上的代码并没有使用线程池,接下来让我们分析一下问题
第一个客户端连接之后,accept就返回了,得到了一个clientSocket,进入了processConnection
接下来又进入了一个while循环,在这个循环中,需要反复处理客户端发来的请求数据,如果客户端这会还没有发送请求,服务器代码就会阻塞在sc.hasNext这里
此时此刻,第二个客户端来连接了,此时是可以连接成功的(因为连接由内核负责)建立连接之后,连接对象就会在内核的队列里等待,直到代码通过accept把连接取出来。
但是当前的代码是无法第一时间执行到第二次的accept。第一个客户端会使服务器处于processConnection内部,直到第一个客户端退出,processConnection才能结束,才能执行后续的操作。
出现这个问题的关键点在于两重循环在一个线程里,进入第二重循环的时候,无法继续执行第一个循环,UDP服务器中只有一个循环,故不会出现这个问题,所以我们应使用多线程来解决。
![image-20231022131028252](https://image.33mc.cn/luntan/202310221320071.webp)
网络原理
一.UDP
1.1 UDP报文结构
UDP是传输层协议之一,其主要特点是无连接,不可靠传输,面向数据报,全双工
![image-20231026205157171](https://image.33mc.cn/luntan/202310262051489.webp)
UDP报文主体分为两个部分:UDP报头(占8个字节)+UDP数据/UDP载荷
UPD报头:源端口号+目的端口号+数据报长度+校验和
源端口号和目的端口号
- 源端口号和目的端口号均占用16个比特位,即为2个字节
UDP长度
- 总共16位,占两个字节
- UDP报文长度=UDP报头(首部)+UDP载荷
- 2个字节能表示的数据范围是0~65535,也就是能够表示的报文长度是65536字节(Byte),转换成KB,65536/1024 = 64 KB 这就是一个UDP报文所能表示的最大长度.
校验和
- 数据在传输的时候,本质上是0/1bit流,通过光信号或者电信号来表示,如果在传输的时候收到干扰,就可能会出现比特翻转现象.这个时候就需要校验和校验数据是否出错.
1.2UDP报文特点
1.无连接
知道对端的 IP 和端口号就直接进行传输,不需要建立连接;
2.不可靠
没有任何安全机制,发送端发送数据报以后,如果因为某些问题无法发送到对端, UDP 协议层也不会给应用层返回任何错误信息;
3.面向数据报
应用层无论交给 UDP 多大的报文,UDP原样发送,不会拆分或者合并;
4.缓冲区
UDP 只有接收缓冲区,没有发送缓冲区
5.大小受限
UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K (包含 UDP首部)。
1.3 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议