目录
一 前言
1.为什么需要网络编程?
在生活中,我们可以通过网络获取丰富多彩的网络资源。比如,我们在B站上看到的各种视频,实质都是通过网络获取到的资源。而所有的网络资源,都是通过网络编程来进行数据传输的。
2.网络编程的基本概念
① 网络编程:指网络上的主机通过不同的进程,以编程的方式实现网络通信(网络数据传输)。
②基本概念:
- 发送端和接收端
发送端:数据的发送方进程,即源主机。
接收端:数据的接收方进程,即目的主机。
收发端:发送端和接收端的两端。
- 请求和响应
一般获取一个网络资源,需要涉及到两次网络数据传输:
第一次:请求数据发送
第二层:响应数据发送
- 客户端和服务端
服务端:提供服务的一方进程。
客户端:获取服务的一方进程。
二 Socket套接字
①概念:
由系统提供用于网络通信技术,基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
②分类:
针对传输层协议分为三类:
流套接字:使用传输层TCP协议
流式数据的特征就是在IO流没有关闭的情况下,可以多次发送,也可以分开多次接收。
数据报套接字:使用传输层UDP协议
对于数据报来说,传输数据是一块一块的,假如发送一块100个字节的数据,那么发送和接收必须都是一次100字节,而不能分100次发送,每次接收一字节。
原始套接字:用于自定义传输层协议,用于读写内核没有处理的IP协议数据
③注意事项:
1、客户端与服务端在开发时是基于一个主机的两个进程,而在真实场景中,一般都是不同主机。
2、目的IP和目的端口号标识了数据传输时要发送到的目的主机和进程。
3、Socket编程是基于传输层的TCP/UDP协议使用的流套接字和数据报套接字,但应用层协议也要考虑。
三 UDP数据报套接字编程
UDP:User Datagrm Protocol(用户数据报协议)
特点:无连接,不可靠传输,面向数据报,全双工
1.DatagramSocket API
用于发送和接收UDP数据报,代表一个socket对象。
构造方法:
方法:
2.DatagramPacket API
代表一个UDP数据报,构造对象,在指定字节数组作为缓存区。
构造方法:
方法:
3.InetSocketAddress API
构造方法:
示例一:一发一收(无响应)
以下为一个客户端一次数据发送和服务端多次数据接收(只有客户端请求,没有服务端响应)
UDP服务端
public class UdpServer { //服务器socket绑定固定的端口 private static final int PORT = 8888; public static void main(String[] args) throws IOException { //1.创建服务端DatagramSocket,指定端口,可以发送并接收UDP数据报 DatagramSocket socket = new DatagramSocket(PORT); //只要有客户端的数据报就要接收 while(true){ //2.创建数据报,用于接收客户端发送的数据 byte[] bytes = new byte[1024]; DatagramPacket packet = new DatagramPacket(bytes, bytes.length); System.out.println("-------------------"); System.out.println("等待接收UDP数据报..."); //3.等待客户端发送的UDP数据报,在接收到数据报之前一直阻塞,接收到数据报以后,DatagramPacket对象包含数据(bytes),客户端ip,端口号 socket.receive(packet); System.out.printf("客户端IP:%s%n", packet.getAddress().getHostAddress()); System.out.printf("客户端端口号:%s%n", packet.getPort()); System.out.printf("客户端发送的原生数据:%s%n", Arrays.toString(packet.getData())); System.out.printf("客户端发送的文本数据为:%s%n", new String(packet.getData())); } } }
UDP客户端
public class UdpClient { //服务端socket地址:域名或IP,端口号 private static final SocketAddress ADDRESS = new InetSocketAddress("localhost",8888); public static void main(String[] args) throws IOException { //创建客户端DatagramSocket,开启随意端口,可以发送及接收数据报 DatagramSocket socket = new DatagramSocket(); //准备要发送的数据 byte[] bytes = "hello world!".getBytes(); //组装要发送的UDP数据报,包含数据,服务器IP,端口号 DatagramPacket packet = new DatagramPacket(bytes, bytes.length, ADDRESS); //发送UDP数据报 socket.send(packet); } }
示例二:请求响应
对示例一进行改造:构造一个展示服务端本地某个目录(BASE_PATH)的下一子级文件列表的服务。
1.客户端先接收键盘输入,表示要展示的相对路径
2.发送请求:将该相对路径作为数据报发送到服务端
3.服务端接收并处理请求:根据请求数据,作为本地目录的路径,列出下一子级文件及文件夹
4.服务端返回响应:遍历子文件和文件夹,每个文件每一行作为响应的数据报返回给客户端
5.客户端接收响应:打印输出响应内容
为了解决空字符或长度不足数据丢失的问题,客户端服务端约定好统一的协议:这里简单的设计为 ASCII结束字符 \3 表示报文结束。UDP服务端:public class UdpServer { //服务器socket绑定固定的端口 private static final int PORT = 8888; //本地文件目录要展示的根目录 private static final String BASE_PATH = "D:/TEST"; public static void main(String[] args) throws IOException { //1.创建服务端DatagramSocket,指定端口,可以发送并接收UDP数据报 DatagramSocket socket = new DatagramSocket(PORT); //不停接收客户端udp数据报 while(true){ //2.创建数据报,用于接收客户端发送的数据 byte[] requestData = new byte[1024]; DatagramPacket requestpacket = new DatagramPacket(requestData, requestData.length); System.out.println("-------------------"); System.out.println("等待接收UDP数据报..."); //3.等待客户端发送的UDP数据报,在接收到数据报之前一直阻塞,接收到数据报以后,DatagramPacket对象包含数据(bytes),客户端ip,端口号 socket.receive(requestpacket); System.out.printf("客户端IP:%s%n", requestpacket.getAddress().getHostAddress()); System.out.printf("客户端端口号:%s%n", requestpacket.getPort()); //4.接收到的数据作为请求,根据请求数据执行业务并返回响应 for (int i = 0; i < requestData.length; i++) { byte b = requestData[i]; if(b == '\3'){ //4.1读取请求数据,读取到约定好的结束符(\3),取结束符之前的内容 String request = new String(requestData, 0, i); //4.2根据请求处理业务:本地目录根路径+请求路径,作为要展示的目录列出下一自己文件 //请求的文件列表目录 System.out.printf("客户端请求的文件列表路径:%s%n", BASE_PATH + request); File dir = new File(BASE_PATH + request); //获取下一子级文件,文件夹 File[] children = dir.listFiles(); //4.3构造要返回的响应内容:每个文件及目录名称为一行 StringBuilder response = new StringBuilder(); if(children != null){ for(File child : children){ response.append(child.getName()+"\n"); } } //响应也要约定结束符 response.append("\3"); byte[] responseData = response.toString().getBytes(StandardCharsets.UTF_8); //4.4构造返回响应的数据报DatagramPacket,注意接受的客户端数据包含ip和端口号 DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length, requestpacket.getSocketAddress()); //4.5发送返回响应的数据报 socket.send(responsePacket); break; } } } } }
UDP客户端:
public class UdpClient { //1.服务端socket地址:域名或IP,端口号 private static final SocketAddress ADDRESS = new InetSocketAddress("localhost",8888); public static void main(String[] args) throws IOException { //2.,开启随意端口,可以发送及接收数据报 DatagramSocket socket = new DatagramSocket(); //3.1准备要发送的数据:这里调整为键盘输入作为发送的内容 Scanner sc = new Scanner(System.in); while(true){ System.out.println("-------------"); System.out.println("请输入要展示的目录:"); //3.2每输入的新行就作为要发送的数据报 String request = sc.nextLine()+"\3"; byte[] requestData = request.getBytes(StandardCharsets.UTF_8); //3.3组装要发送的UDP数据报:包含数据,服务器IP,端口号 DatagramPacket requestpacket = new DatagramPacket(requestData, requestData.length, ADDRESS); //4.发送UDP数据报 socket.send(requestpacket); //5.接收服务端响应的数据报,并根据响应内容决定下一个步骤 //5.1创建数据报,用于接收服务端发送的响应 byte[] responseData = new byte[1024]; DatagramPacket responsepacket = new DatagramPacket(responseData, responseData.length); //5.2接收响应数据报 socket.receive(responsepacket); System.out.println("该目录下的文件列表为:"); //byte下次解析的起始位置 int next = 0; for (int i = 0; i < responseData.length; i++) { byte b = responseData[i]; if(b == '\3') break; if(b == '\n'){ String filename = new String(responseData, next, i-next); System.out.println(filename); next = i+1; } } } } }
示例三:回显服务器(echo server)
说明:回显服务器省略了"根据请求计算响应”,请求是什么,响应就是什么。
UDP服务器:
//UDP版本的回显服务器 public class UdpEchoServer { //1.建立一个socket对象并绑定具体端口号 private DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); } public void start() throws IOException { System.out.println("服务器启动!"); while(true){ //2.读取客户发过来的请求 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); //3.根据请求计算响应,由于此处是回显服务器,请求和响应相同 String response = process(request); //4.把响应写回到客户端,send的参数是DatagramPacket,需要用响应数据构造好packet对象 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket); //5.打印一下,当前这次请求响应的中间结果 System.out.printf("[%s:%d]req: %s; resp:%s\n", responsePacket.getAddress().toString(), requestPacket.getPort(), request, response); } } //process:根据请求计算响应 public String process(String request){ return request; } public static void main(String[] args) throws IOException { //端口号可以随便指定:1024-65535 UdpEchoServer server = new UdpEchoServer(9090); server.start(); } }
UDP客户端:
//UDP版本的回显客户端 public class UdpEchoClient { private DatagramSocket socket = null; private String serverIP = null; private int serverPort = 0; /*一次通信需要两个ip,两个端口 客户端的ip是127.0.0.1已知,port是系统自动分配的 服务端的ip和端口也要告诉客户端 */ 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("客户端启动!"); Scanner sc = new Scanner(System.in); while(true){ //1.从控制台读取要发送的数据 System.out.print("> "); String request = sc.next(); 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.读取服务器的UDP响应并解析 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); socket.receive(responsePacket); 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 client = new UdpEchoClient("127.0.0.1",9090); client.start(); } }
四 TCP流套接字编程
TCP:Transmission Control Protocol(传输控制协议)
特点:有连接,可靠传输,面向字节流,全双工
1.ServerSocket API
专门给服务器用的,accept用来接收一个连接
构造方法:
方法:
2..Socket API
服务器和客户端都会用到:客户端使用Socket和服务器建立连接,并且后续进行传输;服务器使用Socket和客户端进行交互。
构造方法:
方法:
Socket提供了getInputStream和getOutputStream获取输入输出流,进一步通过这些流对象来完成数据传输。
TCP中的长短连接
短连接:每次接收到数据并返回响应后,都关闭连接。(只能一次收发数据)
长连接:不关闭连接,一直保持连接状态,双方不停地收发数据。(多次收发数据)
区别:
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;长连接可以是客户端主动发送请求,也可以是服务端主动发送。
- 使用场景不同:短连接适合客户端请求频率不高的场景,比如浏览网页;长连接适用于客户端与服务端通信频繁的场景,如游戏。
- 建立连接、关闭连接耗时:短连接每次请求、响应都需要建立连接、关闭连接;而长连接只需要建立一次。相比之下,长连接效率更高。
示例一:一发一收(短连接)
TCP服务端:
public class TcpServer { //服务器socket要绑定固定端口 private static final int PORT = 8888; public static void main(String[] args) throws IOException { //1.创建一个服务端ServerSocket,用于收发TCP报文 ServerSocket server = new ServerSocket(PORT); while(true){ System.out.println("-------------------"); System.out.println("等待客户端建立TCP连接..."); //2.等待客户端连接,注意该方法为阻塞方法 Socket client = server.accept(); System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress()); System.out.printf("客户端端口号:%s%n",client.getPort()); //3.接收客户端数据,需要从客户端Socket中的输入流获取 System.out.println("接收到的客户端请求:"); InputStream is = client.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; while((line = br.readLine()) != null){ System.out.println(line); } //4.双方关闭连接:服务端关闭的是客户端socket连接 client.close(); } } }
TCP客户端:
public class TcpClient { //服务端IP或域名 private static final String SERVER_HOST = "localhost"; //服务端Socket进程的端口号 private static final int SERVER_PORT = 8888; public static void main(String[] args) throws IOException { //1.创建一个客户端流套接字,并与对应的IP主机对应端口的进程建立连接 Socket client = new Socket(SERVER_HOST, SERVER_PORT); //2.发送TCP数据,通过socket中的输出流进行发送 OutputStream os = client.getOutputStream(); PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8")); //3.1发送数据 pw.println("hello world!"); //3.2有缓冲区的IO操作,真正传输数据,需要刷新缓冲区 pw.flush(); //3.3双方关闭连接:客户端关闭Socket连接 client.close(); } }
示例二:请求响应(短连接)
示例一只是客户端发出请求和服务端接收请求,并没有包含服务端的返回响应,以下是对请求和响应做出的改进:
构造一个展示服务端本地某个目录( BASE_PATH )的下一级子文件列表的服务TCP服务端:
public class TcpServer { //服务器socket要绑定固定端口 private static final int PORT = 8888; private static final String BASE_PATH = "D:/TEST"; public static void main(String[] args) throws IOException { //1.创建一个服务端ServerSocket,用于收发TCP报文 ServerSocket server = new ServerSocket(PORT); while(true){ System.out.println("-------------------"); System.out.println("等待客户端建立TCP连接..."); //2.等待客户端连接,注意该方法为阻塞方法 Socket client = server.accept(); System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress()); System.out.printf("客户端端口号:%s%n",client.getPort()); //3.接收客户端数据,需要从客户端Socket中的输入流获取 InputStream is = client.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8")); String request = br.readLine(); System.out.printf("客户端请求的文件列表路径为:%s%n",BASE_PATH+request); File dir = new File(BASE_PATH+request); File[] children = dir.listFiles(); OutputStream os = client.getOutputStream(); PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8")); if(children != null){ for (File child : children){ pw.println(child.getName()); } } pw.flush(); client.close(); } }
TCP客户端:
public class TcpClient { //服务端IP或域名 private static final String SERVER_HOST = "localhost"; //服务端Socket进程的端口号 private static final int SERVER_PORT = 8888; public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); while(true){ System.out.println("------------"); System.out.println("请输入要展示的目录:"); String request = sc.nextLine(); Socket socket = new Socket(SERVER_HOST,SERVER_PORT); OutputStream os = socket.getOutputStream(); PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8")); pw.println(request); pw.flush(); System.out.println("接收到服务端响应"); InputStream is = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8")); String line; while((line = br.readLine()) != null){ System.out.println(line); } socket.close(); } } }
示例四:
TCP服务端:
TCP服务器如果不使用多线程可能会无法处理多个客户端。因为accept和read也会阻塞,如果其中一处阻塞另一处也就无法处理数据,使用多线程就是为了规避阻塞问题。而UDP只有receive一处阻塞,就不存在上述问题了。
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("启动服务器!"); //此处CashedThreadPool ExecutorService threadPool = Executors.newCachedThreadPool(); while(true){ //使用clientSocket和具体的客户端交流 Socket clientSocket = serverSocket.accept(); //使用线程池 threadPool.submit(()->{ processConnection(clientSocket); }); } } //使用这个方法来处理一个对应到客户端的连接,可能会涉及多次交互 private void processConnection(Socket clientSocket) { 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 sc = new Scanner(System.in); if(!sc.hasNext()){ //没有下一个数据,说明客户关闭了连接 System.out.printf("[%s:%d]客户端下线!\n",clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request = sc.next(); //2.根据请求构造响应 String response = process(request); //3.返回响应结果 //OutputStream没有write String功能,可以把String里的字节数组拿出来进行写入或用字符流转换 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); //flush刷新缓冲区,保证当前写入的数据确实发送出去了 printWriter.flush(); System.out.printf("[%s:%d]req: %s; resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request,response); } }catch (IOException e){ e.printStackTrace(); }finally { //把close放到finally里面,保证一定能执行 try{ clientSocket.close(); }catch (IOException e){ e.printStackTrace(); } } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }
TCP客户端:
public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException{ socket = new Socket(serverIp, serverPort); } public void start(){ System.out.println("客户端启动!"); Scanner sc = new Scanner(System.in); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){ while(true){ //1.先从键盘上读取用户输入的内容 System.out.print("> "); String request = sc.next(); if(request.equals("exit")){ System.out.println("goodbye"); break; } //2.把读到的内容构造成请求,发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request); printWriter.flush(); //3.读取服务器响应 Scanner respScanner = new Scanner(inputStream); String response = respScanner.next(); //4.把响应内容显示到界面上 System.out.println(response); } }catch (IOException e){ e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } }