UDP和TCP网络编程
- UDP网络编程
- UDP通信流程(回显服务)
- 测试
- 扩展《UDP字典查找单词》
- TCP网络编程
- TCP互相通信
- 测试
- 缓存区和缓存
UDP网络编程
特点:
- 无连接:发送数据前不需要建立连接。
- 不可靠:没有重发机制,无法保证数据的可靠性和顺序。
- 无拥塞控制:发送方发送数据的速度受限于网络的带宽。
- 快速:由于没有连接建立和拥塞控制的处理,UDP的传输速度通常比TCP快。
- 简单:UDP协议的头部较小,仅有8个字节的固定长度头部。
UDP通信流程(回显服务)
实现服务层步骤
-
创建一个socket用于通信
DatagramSocket
-
等待浏览器分配端口
port
-
使用socket读取用户的请求
-
启动服务器后如果没有客户端发来请求,则会一直阻塞等待
-
首先需要创建一个容器
packet
s设置容器大小以字节数组为单位 -
然后使用socket接收
socket.receive(packet);
-
接收以后,此时的packet中就保存了用户发来的请求
-
为了方便处理,使用字符串来接收该请求取名为
request
-
-
根据请求计算响应数据,因为我们的服务是回显服务,所以只需要返回请求即可
response = request
-
把响应结果返回给客户端,和刚刚封装请求一样,这里也同样使用一个
DatagermPacket
来构造响应数据。- 这里的响应取名为
responsePacket
,需要将客户端的Ip、端口
一并与响应数据进行封装 - 最后使用socket发送响应数据包
socket.send(responsePacket);
- 这里的响应取名为
-
打印数据(非必须)
服务器具体代码实现:
public class DirServer {
//创建一个socket
DatagramSocket socket =null;
public DirServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器总逻辑
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//每次循环
//1. 读取请求并响应
DatagramPacket packet = new DatagramPacket(new byte[1024],1024);
//从socket中获取数据存储在packet中
socket.receive(packet);
//拿着packet中的字节进行构造
String request = new String(packet.getData(),0,packet.getLength());
//2. 根据请求计算响应
String response = process(request);
//3. 把响应结果返回给客户端
// 和请求packet不同,这里构造响应时需要指定目的地,也就是发送请求的客户端的地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,packet.getSocketAddress());
socket.send(responsePacket);
//打印请求数据和响应数据
System.out.printf("[%s:%s]req : %s,resp:%s\n",packet.getAddress(),packet.getPort(),request,response);
}
}
//构造响应数据
public String process(String request) {
return request;
}
//主函数
public static void main(String[] args) throws IOException {
DirServer dirServer = new DirServer(9090);
dirServer.start();
}
实现客户端步骤
-
与服务端相同,这里需要指定一个socket来发送请求,socket需要设置服务器的
IP和端口
-
输入请求,这里是从控制台获得请求数据使用输入方法
Scanner
- 同样的将这个字符串构造成一个字符串
-
把从控制台读取的字符串进行打包,使用
DatagramPacket
- 将请求数据与端口信息和IP地址一同打包在一个
requestPacket
中 - 发送请求使用
socket.send(requestPacket)
- 等待服务器的响应
- 将请求数据与端口信息和IP地址一同打包在一个
-
接收响应数据,也需要使用一个
DatagramPacket
设置容器的大小用于接收响应socket.receive(responsePacket);
-
将响应数据转化为字符串进行显示
response
-
打印请求数据与响应数据
客户端具体代码实现:
public class DirClient {
//设置一个全局变量
private DatagramSocket socket = null;
private String serverIp;//IP地址
private int port;//端口号
//构造方法,将IP和端口号传入socket中,连接服务器
public DirClient(String serverIP,int serverPort) throws SocketException {
//对于客户端来说,不需要显示关联窗口
//不过也需要端口,系统会根据需求自动分配一个端口
socket = new DatagramSocket();
this.serverIp = serverIP;
this.port = serverPort;
}
//启动方法
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true){
//1. 先从控制台读取一个字符串
// 打印一个提示符,提示用户需要输入的内容
System.out.print("--》");
String request = scanner.next();
//2. 把字符串构成UDP packet进行发送
// 通过packet打包,需要发送的ip地址及端口号分别设置值,
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), port);
socket.send(requestPacket);
//3. 客户端尝试收取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
//4.把响应数据转换为String进行显示
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
System.out.printf("req: %s,resp : %s \n",request,response);
}
}
//主函数
public static void main(String[] args) throws IOException {
DirClient udpClient = new DirClient("127.0.0.1",9090);
udpClient.start();
}
}
测试
🕵️♂️先启动服务器,再打开客户端,可以设置一个输入exit
就退出的逻辑。
在输入时判断输入的是否为exit
Scanner scanner = new Scanner(System.in);
String request = scanner.next();
if(request.equals("exit"))break;
服务器打印
客户端打印
扩展《UDP字典查找单词》
单纯使用一个UDP字典进行回显未免有点无聊😩
反正UDP是进行回显,不如回显一些更有意思的事情,比如困恼我们许久的单词!
- 设计思路
- 使用一个map来保存单词和翻译
- 读取客户端的单词
- 在map中查找单词的翻译返回响应
我们基于刚刚实现的回显程序,只需要改动一下响应数据即可。
public class AddDir extends DirServer{
HashMap<String,String> dictionary = new HashMap<>();
//需要在构造时把单词和翻译写入map
//读者可根据需要自行修改
public AddDir(int port) throws SocketException {
super(port);
dictionary.put("programmer","程序员");
dictionary.put("great","厉害");
dictionary.put("programming","编程");
}
@Override
public String process(String request){
return dictionary.getOrDefault(request,"没有这个单词");
}
public static void main(String[] args) throws IOException {
AddDir addDir = new AddDir(9090);
addDir.start();
}
}
测试:
客户端(出现乱码,是因为使用的字符集不匹配,可以在响应数据中设置编码格式解决)
服务器:
TCP网络编程
TCP的特点
- 面向连接:发送数据前必须先与对方建立连接。
- 可靠:具有数据的完整性保护和包的顺序控制,能够确保数据的可靠性和正确性。
- 有拥塞控制:TCP会通过动态调整发送数据的速度,防止网络拥塞。
- 慢速:连接的建立需要时间,有较长的等待时间和握手过程,传输速度相对缓慢。(三次握手)
- 复杂:TCP协议的头部较大,有20个字节的固定长度
TCP互相通信
实现服务器步骤:
-
TCP与UDP不同,需要连接,启动服务器需要传递
IP
。TCP需要使用另外一个Socket来管理一个用户,所以第一步需要创建两个不同的socket服务器。- 外层
ServerSocket
主要负责对业务处理 - 内层
socket
是将请求统一打包给服务器内部 - 实际ServerSocket只有一个,其他的都是由这个accept分离出去的socket
- 外层
-
通过接收请求的方法
accept
获取新socket
-
创建一对输入输出字符流,使用输入流去读取请求信息,使用输出流返回响应到客户端
inputStream
输入流接收请求outputStream
输出流返回响应
-
读取请求数据,请求和响应都可能有多个
-
根据请求数据计算响应数据,为了更简单的包装请求,不需要一个一个字节去计算,所以这里设置为字符串类型
-
返回响应数据到客户端
PrintWriter.println(响应数据)
-
打印信息
服务端具体代码实现:
public class TCPEchoServer {
//外层socket
ServerSocket socket = null;
//构造方法
public TCPEchoServer(int port) throws IOException {
socket = new ServerSocket(port);
}
//启动方法
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//接收到一个请求
Socket accept = socket.accept();
processConnection(accept);
}
}
//通过这个方法处理一个连接
public void processConnection(Socket clientSocket) throws IOException {
System.out.println("客户端上线!"+clientSocket.getLocalSocketAddress().toString());
//创建一个输入流和输出流
//在这里需要简单约定一下,每个请求使用\n来分割
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//创建一个字符流,接收响应数据
//
Scanner scanner = new Scanner(inputStream);//相当于字符流
PrintWriter writer = new PrintWriter(outputStream,true);
while (true){
//1. 读取请求
if(!scanner.hasNext()){
//读取数据结束,(对端关闭)
System.out.println("客户端下线!"+clientSocket.getInetAddress().toString());
break;
}
String request = scanner.next();
//2. 根据请求计算响应(调用响应方法)
String response = process(request);
//3. 把响应返回给客户端(一个或多个请求或响应)
writer.println(response);
System.out.println("address:"+clientSocket.getInetAddress()+"port: "+clientSocket.getPort()+" req:"+request+" reps: "+response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
clientSocket.close();
}
}
//返回响应数据
private String process(String request) {
Scanner scanner = new Scanner(System.in);
System.out.println("请求:"+ request+" 你的回复:");
String response = scanner.next();
return response;
}
//主函数
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9090);
tcpEchoServer.start();
}
}
实现客户端步骤:
- TCP可以连接多个客户端进行通信,客户端连接服务器需要创建一个
socket
- 使用
InputStream
在控制台接收用户输入的数据 - 使用
PrintWriter
进行封装数据作为客户端请求request
- 发送客户端请求
printWriter.println(request);
- 从服务器接收响应数据。使用一个字符串用于接收响应
response
- 打印响应数据
具体代码实现:
public class TCPEchoClient {
//设置一个全局socket
private Socket socket = null;
public TCPEchoClient(int port,String serverIp) throws IOException {
socket = new Socket(serverIp,port);
}
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerFromSocket = new Scanner(inputStream);
//注意:这里有一个ture意味着刷新缓存区
PrintWriter printWriter = new PrintWriter(outputStream,true);
while (true){
//1.从键盘读取请求(用户输入的内容)
System.out.println("-> ");
String request = scanner.next();
//2.将读取的请求,发送给服务器
//无换行内容
printWriter.println(request);
//3.从服务器读取响应内容
String response = scannerFromSocket.next();
//4.把响应数据结果显示到控制台
System.out.println("req: "+request+"resp:"+response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
}
}
//主函数
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient(9090,"127.0.0.1");
tcpEchoClient.start();
}
}
测试
服务器
客户端
缓存区和缓存
在TCP通信中,如果没有加fulsh
刷新缓存区就无法即使读取请求数据,也无法及时收到响应数据,那么这是什么原因导致的呢?
其实,在内存中存在缓存区,缓存区是相当于池子
一样的,可以用于存放数据的容器。 而与我们经常提到的缓存是两个不一样的东西,缓存是一种计算机系统的技术。
缓存:
用于提高数据读取和写入效率。当计算机需要执行或者读取某个文件时,会将一部分数据加载到缓存中,下一次需要的时候,直接从缓存中获取即可,避免了不必要的磁盘读写操作,提高访问速度。
缓存区:
计算机分配的一块内存空间,用来存储数据,通常是临时存储,在进行IO操作时,当数据读取到了缓存区,可以由程序直接操作,提高了数据读取效率。
TCP中printWrite
首先是在硬盘中写入数据(请求),为了提高效率,数据被写入缓存区,缓存区有一个特性就是满了才会将数据进行统一提交(提交到网卡),这样才能读到请求。显然,这是不现实的,所以这里我们需要进行刷新(Flush)操作。
可使用两种方法进行刷新:
自动刷新(构造PrintWrite时设置):
PrintWriter printWriter = new PrintWriter(outputStream/*输出流*/,true);
手动刷新(需要设置在客户端发送请求数据后):
printWriter.flush();