在我们初识完网络知识后,我们了解了关于TCP/IP五层模型
分别是:应用层,传输层,网络层,数据链路层,物理层
这篇文章,我们主要了解关于传输层中相关的俩种协议:UDP TCP
1.UDP和TCP的特点
2.UDP网络编程
3.TCP网络编程
在传输层中,传输层为应用层提供了编程接口(即一组类或者方法)给我们使用 ---- socket
提供了俩组socket,分别是UDP和TCP
UDP和TCP的特点
传输层涉及到了俩种最主要的协议,UDP协议和TCP协议
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
有连接 / 无连接
连接就是让俩台主机能够互相的进行通信,互相保存了对方的核心信息(IP和端口号)
断开连接就是删除对方的核心信息
TCP在正式通信之前,就需要保存好双方的关键信息,通信完毕之后再进行删除
UDP在正式通信之前,就不需要
可靠传输 / 不可靠传输
我们在进行网络通信的过程,往往有时候会出现丢包的现象(正常现象)
TCP是可靠传输,他就是在对抗丢包现象的出现,尽可能的将数据包传输给对方,但是有时候还是会出现丢包的现象,无法达到100%的传输,但也可以知道对方是否接收到了数据包
UDP则不考虑是否丢不丢包的情况,爱咋咋地吧~~~~这样传输效率就会比较高,而TCP传输效率就会比较低,毕竟他需要要关注丢包的情况
面向字节流 / 面向数据报
面向字节流:与读文件类似,读写的方式非常的灵活,可以按照规定的字节,分多次进行读取
面向数据报:每次读和写,要以一个UDP数据报为单位进行
全双工 / 半双工
全双工:一个通信,可以进行双向传输
半双工:一个通信,只能单向传输
UDP网络编程
其中DatagramPacket就是UDP数据传输的基本单位
1.)实现一个回显的UDP客户端和服务器
当客户端发送一串字符串,服务器应该原封不动的返回,然后输出在显示器上 ---- 回显
服务器端Code:
package UDP; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.nio.charset.StandardCharsets; //服务端 public class UdpServer { private DatagramSocket socket = null;//先不初始化 //构造方法 --- port就是端口号 public UdpServer(int port) throws SocketException { socket = new DatagramSocket(port) ; } //启动服务器 public void start() throws IOException { System.out.println("Server Start!!!!"); //因为服务器是7*24h不断工作,等待客户端发来的请求~~~所以使用死循环 while(true) { //1.接收请求并解析 //接收请求,需要先申请一个DatagramPacket内存空间 DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//DatagramPacket是以数据包为单位进行读取~~~ //receive的构造参数时DatagramPacket,所以需要传给他Datagrampacket socket.receive(requestPacket);//客户端请求接收 //因为客户端发送的请求中是二进制数组,我们需要将DatagramPacket转换为字符串 ---- 获取二进制数据,并从下标0到length转换为字符串 String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //2.根据请求计算响应(因为我们只需要在显示器打印东西,所以不需要计算) //根据请求计算相应,这里我们实现的是回显操作,客户端发送什么,服务器就返回什么即可~~~~ String response = process(request); //3.将响应重新返回给客户端 //需要DatagramPacket,传字节数组,长度,getSocketAddress(会返回ip和port),相应也是按照DatagramPocket数据包为单位进行发送~~~~ DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress()); //send构造参数也是DatagramPacket,所以需要我们将字符串转换为DatagramPacket socket.send(responsePacket); //4.打印(打印ip,端口号,请求内容,相应内容) System.out.printf("[%s : %d] request : %s, resopnse : %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); } } public String process(String request) { return request;//因为这里是回显操作,客户端发送什么,服务器返回什么即可,不需要计算的操作 } public static void main(String[] args) throws IOException { UdpServer server = new UdpServer(10086); server.start(); } }客户端Code:
package UDP; import java.io.IOException; import java.net.*; import java.util.Scanner; //客户端 public class UdpClient { private DatagramSocket socket = null; private String serverIp; private int serverPort; public UdpClient(String serverIp, int serverPort) throws SocketException { socket = new DatagramSocket(); this.serverIp = serverIp; this.serverPort = serverPort; } public void start() throws IOException { System.out.println("client start!!!"); Scanner scanner = new Scanner(System.in); while(true) { //1.让用户从控制台输入字符串 System.out.print("请输入一串字符串:"); String request = scanner.next(); if(request.equals("exit")) { break; } //2.将用户输入的字符串转换为UDP数据包类型,因为send的类型是数据包类型~~~ DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIp), this.serverPort); socket.send(requestPacket);//将请求发送给服务器 //3.获取服务器的响应 --- receive构造也是DatagramPacket,所以也需要构造一个DatagramPacket,注意这里获取的是字节数组 DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024); //接收服务器响应 socket.receive(responsePacket); //转换为字符数组,因为服务器发送的响应是一个二进制字节数组,我们需要转换为String,然后打印的显示器上 String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); //4.显示响应 System.out.println("响应数据为:" + response); } } public static void main(String[] args) throws IOException { UdpClient client = new UdpClient("127.0.0.1", 10086);//告诉客户端服务器的ip和port client.start(); } }这里有几点要注意:
1.我们让用户输入的是字符串,但是DatagramPacket参数是二进制,所以需要将字符串转换为二进制
2.服务器返回的是二进制,客户端需要接收并且转换为字符串形式,才能显示在显示器上,否则就是乱码
3.服务器应该设置为While死循环,因为服务器是7*24小时服务的
4.在编写服务器的时候,需要我们程序员指定的给定一个端口号,而客户端是不需要给定端口号的,是一个无参数的构造方法,操作系统会自动分配一个空闲的端口号
5.服务器需要接收客户端的请求并且解析 ---> 根据请求计算响应 ----> 将响应返回给客户端
6.客户端需要从控制台读取用户输入的字符串 ---> 构造请求发送给服务器 ---> 读取服务器返回的响应 ---> 显示响应结果到显示器上
2.)实现一个查字典的UDP客户端和服务器
客户端输入一个中文单词,服务器要求返回这个中文对应的英文单词,然后客户端接收,并且显示到显示器上
这里就会使用Map保存好中文和对应的英文单词~~~
服务器Code:
package UDP; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.util.HashMap; import java.util.Map; //翻译服务器 public class UdpDictionaryServer { private DatagramSocket socket = null; //创建词典 public Map<String, String> map = new HashMap<>(); public UdpDictionaryServer (int port) throws SocketException { socket = new DatagramSocket(port); map.put("苹果", "apple"); map.put("香蕉", "banana"); map.put("猪", "pig"); map.put("鸭子", "duck"); map.put("老虎", "tiger"); map.put("鱼", "fish"); map.put("你好", "hello"); } public void start() throws IOException { System.out.println("Server Start~~~"); while(true) { //1.接收请求并解析 --- 需要使用receive接收,receive的构造方法是DatagramPakcet DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024); socket.receive(requestPacket); //receive接收到的是一个二进制字节数组,需要转换为字符串 String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); //2.根据请求计算响应 String response = process(request); //3.计算好的响应返回给客户端 -- 需要使用send,但是注意,send构造也是需要DatagramPacket的~~~,这里参数是获取字节数组,字节数组长度,以及接收到请求的IP和Port DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); socket.send(responsePacket); //4.显示打印 System.out.printf("[%s : %d] request : %s, resopnse : %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); } } public String process(String request) { return map.getOrDefault(request, "很抱歉没查找到此单词~~~"); } public static void main(String[] args) throws IOException { UdpDictionaryServer server = new UdpDictionaryServer(10086); server.start(); } }客户端Code:
package UDP; import java.io.IOException; import java.net.*; import java.util.Scanner; //翻译客户端 public class UdpDictionaryClient { DatagramSocket socket = null; private String ip; private int port; public UdpDictionaryClient(String ip, int port) throws SocketException { socket = new DatagramSocket(); this.ip = ip; this.port = port; } public void start() throws IOException { System.out.println("Client Start~~~"); Scanner scanner = new Scanner(System.in); while(true) { System.out.print("请输入中文:"); String dictionary = scanner.next(); if(dictionary.equals("exit")) { break; } //1.发送请求给服务器 --- 需要使用到send,构造方法是DatagramPacket DatagramPacket request = new DatagramPacket(dictionary.getBytes(), dictionary.getBytes().length, InetAddress.getByName(this.ip), this.port); socket.send(request); //2.接收服务器的响应 --- 需要使用到receive,构造方法是DatagramPacket DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024); socket.receive(responsePacket); //因为服务器响应回来的是一个二进制字节数组,我们需要转换为字符串 String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); System.out.println("翻译后结果为:" + response); } } public static void main(String[] args) throws IOException { UdpDictionaryClient client = new UdpDictionaryClient("127.0.0.1",10086); client.start(); } }TCP网络编程
TCP提供了俩个类:ServerSocket(给服务器使用) 和 Socket(即给客户端,也给服务器使用)
ServerSocket的任务是:负责将连接到服务器的客户端进行统一处理
Socket的任务是:针对每一个客户端,提供具体的服务(读写数据)
1.)实现回显的TCP客户端和服务器
服务器Code:
package TCP; 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; public class TcpServer { //创建ServerSocket对象 private ServerSocket serverSocket = null; public TcpServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("Server Start~~~"); while(true) { //需要接收客户端的连接 --- 需要使用到accept,如果接收到了,会返回,否则就一直阻塞等待 Socket socket = serverSocket.accept();//这里的socket会保存客户端的ip和port,每一次连接客户端,就会创建一个socket连接,但是很快就执行完,所以执行完后需要关闭连接~~ //处理连接过程,但是服务器只有一个,当我们启动多个客户端,一个服务器无法接收多个客户端的请求,这时就需要创建多线程,让多线程去实现多个服务器,才能接收不同客户端的请求 Thread t = new Thread(() -> { try { processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } } public void processConnection(Socket socket) throws IOException { //一次连接的过程中,客户端和服务器可能会传输多组数据 System.out.printf("【%s : %d】 客户端上线\n", socket.getInetAddress(), socket.getPort()); //因为TCP使用的是字节流,所以我们可以使用InputStram---读,OutputStream---写 try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); PrintWriter write = new PrintWriter(outputStream); while(true) { //1.处理多次请求,这里一次循环就处理一个请求 if(!scanner.hasNext()) { System.out.printf("【%s : %d】 客户端下线\n",socket.getInetAddress(), socket.getPort()); break; } String request = scanner.next(); //2.根据请求计算响应 --- 这里实现的是回显操作,直接返回客户端请求即可 String response = process(request); //3.将响应返回给客户端 write.println(response); //刷新缓冲区 write.flush(); //4.打印日志 System.out.printf("【%s : %d】 request ->%s response ->%s\n", socket.getInetAddress(), socket.getPort(), request, response); } }catch (IOException e) { e.printStackTrace(); } finally { socket.close();//当客户端和服务器连接完毕,并执行完上面代码,这个连接就失效了,我们需要手动关闭socket的连接,防止资源泄露!!!! } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpServer server = new TcpServer(10086); server.start(); } }每次服务器收到一个客户端的连接,都是通过ServerSocket的进行accept
ServerSocket只需要进行accept即可,不负责提供读写的操作
客户端Code:
package TCP; 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 TcpClient { private Socket socket = null; public TcpClient(String ip, int port) throws IOException { socket = new Socket(ip, port); } public void start() { System.out.println("Client Start~~~"); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(System.in); PrintWriter write = new PrintWriter(outputStream); Scanner read = new Scanner(inputStream); while(true) { //1.让用户输入字符串 System.out.print("请输入一串字符串 ->"); String request = scanner.next(); //2.将请求发送给服务器 write.println(request); //刷新缓冲区 write.flush(); //3.接收服务器响应 if(!read.hasNext()) { break; } String response = read.next(); //将响应输出在屏幕上 System.out.println(response); } }catch(IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpClient client = new TcpClient("127.0.0.1", 10086); client.start(); } }对于TCP的网络编程,有三点需要注意:
1.)TCP是面向字节流的,即InputStream / OutputStream,会对文件进行操作,所以文件内部是设置了缓冲区,我们写入的文件,并不是直接写入文件,而是写入到了一个缓冲区的地方,当缓冲区满了,才会真正写入到文件中 ---- 所以在write的时候,需要使用flush对缓冲区进行刷新,即使缓冲区没有满,也是可以真正写入的
2.)当前代码中是存在文件资源泄露的情况,因为当客户端每次连接上服务器,都会触发一次accept,都会创建一个Socket,就会占用一个文件资源描述符表的位置!!!当客户端源源不断的过来,每一次的连接都会产生一个新的socket文件,而我们只使用,不关闭,就会一直占用文件描述符表的位置 ---- 这时就需要我们进行close操作
3.)我们需要使用多线程,才能让多个客户端同时进行操作,如果随着客户端数量不断增加,我们后续还可以继续考虑线程池,减小线程的创建与销毁的操作
2.)实现翻译字典的TCP客户端和服务器
服务器Code:
package TCP; 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.HashMap; import java.util.Map; import java.util.Scanner; public class TcpDictionaryServer { private ServerSocket serverSocket = null; private Map<String, String> map = new HashMap<>(); public TcpDictionaryServer(int port) throws IOException { serverSocket = new ServerSocket(port); map.put("你好", "hello"); map.put("鸭子", "duck"); map.put("鱼", "fish"); map.put("猪", "pig"); map.put("狗", "dog"); map.put("猫", "cat"); } public void start() throws IOException { System.out.println("Server Start~~~"); //与客户端产生连接,连接成功会返回 Socket socket = serverSocket.accept(); Thread t = new Thread(() -> { //使用多线程,让多个客户端可以运行,避免一个服务器只能服务一个客户端 try { processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } public void processConnection(Socket socket) throws IOException { //客户端和服务器一次连接,之间可能传输较多数据 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 write = 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); //将请求返回给客户端 write.println(response); //刷新 write.flush(); //打印日志 System.out.printf("【%s : %d】 request ->%s response ->%s\n", socket.getInetAddress(), socket.getPort(), request, response); } }catch (IOException e) { e.printStackTrace(); }finally { socket.close();//当客户端与服务器连接,执行完上面的代码,连接就失效了,这时需要释放socket,防止资源泄露!!!! } } public String process(String request) { return map.getOrDefault(request, "很抱歉,没查找到此单词~~~"); } public static void main(String[] args) throws IOException { TcpDictionaryServer server = new TcpDictionaryServer(10086); server.start(); } }客户端Code:
package TCP; 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 TcpDictionaryClient { private Socket socket = null; public TcpDictionaryClient(String ip, int port) throws IOException { socket = new Socket(ip, port); } public void start() { System.out.println("Client Start~~~"); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(System.in); PrintWriter write = new PrintWriter(outputStream); Scanner read = new Scanner(inputStream); while(true) { //让用户输入内容 System.out.print("请输入一个中文:"); String request = scanner.next(); //发送请求 write.println(request); write.flush();//刷新缓冲区 //接收服务器响应 if(!read.hasNext()) { break; } String response = read.next(); //将服务器响应输入到显示器上 System.out.println("英文是:" + response); } }catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpDictionaryClient client = new TcpDictionaryClient("127.0.0.1", 10086); client.start(); } }