网络编程
- 了解 TCP/IP 网络四层体系结构
- 了解 TCP 协议和 UDP 协议的区别
- 掌握开发网络应用的Java包:java.net
- 掌握基于 TCP 协议的类
- 掌握基于 UDP 协议的类
- URL,URLConnection 类的编程应用
- Socket 和 ServerSocket 的编程应用
- DatagramPacket,DatagramSocket的编程应用
计算机网络体系结构分层
OSI 7层模型 | TCP/IP 4层模型 | TCP/IP协议簇 |
---|---|---|
应用层 | 应用层 | SMTP、FTP、DNS、SNMP、NFS、HTTP、TELNET |
表示层 | ||
会话层 | ||
传输层 | 传输层 | TCP、UDP |
网络层 | 网际互联层(网络层) | ICMP、IGMP、IP、ARP、RARP |
数据链路层 | 网络接口层(也称为链路层) | LAN、MAN、WAN |
物理层 |
TCP/IP网络四层体系结构
- 应用层
- 对应于 OSI 参考模型的高层,为用户提供所需要的各种服务。
- 运行在计算机操作系统中的各种网络软件就工作在应用层,我们如果用Java开发一个具有网络功能的应用, 也工作在应用层。例如浏览器就是一个在应用层可以解析 HTTP 协议的应用程序。
- 应用层的各种网络应用能够真正实现通信是以下层为基础的。
- 传输层
- 对应于 OSI 参考模型的传输层, 为应用层实现提供端到端的通信功能, 保证了数据包的顺序传送及数据的完整性: 传输控制协议( TCP ) 和用户数据报协议( UDP )。
- TCP 协议提供的是一种可靠的、 通过“三次握手”来连接的数据传输服务; 而UDP协议提供的是不保证可靠(并不是不可靠)、无连接的数据传输服务。
- 网际互联层
- 对应于 OSI 参考模型的网络层,主要解决主机到主机的通信问题。它所包含的协议设计数据包在整个网络上的逻辑传输。注重重新赋予主机一个 IP 地址来完成对主机的寻址, 他还负责数据包在多种网络中的路由。
- 该层有三个主要协议:网际协议( IP )、互联网组管理协议( IGMP )和互联网控制报文协议 ( ICMP ) 。IP 协议是网际互联层最重要的协议, 他提供的是一个可靠、无连接的数据报传递服务。
- 网络接口层
- 与 OSI 参考模型的物理层和数据链路层相对应。 它负责监视数据在主机和网络之间的交换。 事实上, TCP/IP 协议本身并未定义该层的协议 ,而由参与互连的各网络使用自己的物理层和数据链路层协议, 然后与 TCP/IP 的网络接入层进行连接。 地址解析协议( ARP ) 工作在该层, 即 OSI 参考模型的数据链路层。
TCP 协议和 UDP 协议的区别
- 面向连接。 TCP 协议是一种基于连接的协议,发送数据前先建立连接( TCP 有三次握手);UDP 协议是面向无连接的,即发送数据之前不需要建立连接。
- 数据通信的可靠性。TCP 协议可在两台计算机之间提供可靠的数据流,为需要可靠通信的应用应用程序提供了一个点对点通道,没有数据包丢失;UDP 协议提供两个应用程序之间不可靠的通信,有可能会有数据包丢失。
- 对系统资源要求不同。 TCP 协议建立连接需要额外的系统开销; 而 UDP 协议不需要建立连接,但是减少了维持连接的系统开销。
掌握开发网络应用的Java包
基于 TCP 协议的类
- URL
- URLConnection
- Socket
- ServerSocket
基于 UDP 协议的类
- DatagramPacket
- DatagramSocket
- MulticastSocket
URL,URLConnection类的编程应用
-
从 URL 读取信息
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; /** * @Author: Travelmate * @CreateTime: 2021/5/23 16:00 * @Description: */ public class Main { public static void main(String[] args) { URL baidu; try { baidu = new URL("http://www.baidu.com"); // 通过 URL 对象获得输入流, 并封装到一个缓存字符输入流中 BufferedReader in = new BufferedReader(new InputStreamReader(baidu.openStream())); String inputLine; // 每次从字符流读取一行字符串 while ((inputLine = in.readLine()) != null) { // 输入到屏幕 System.out.println(inputLine); } in.close(); } catch (MalformedURLException e) { // 处理 URL 构造方法可能抛出的异常 e.printStackTrace(); } catch (IOException e) { // 处理 openStream()方法可能抛出的异常 e.printStackTrace(); } } }
-
从 URLConnection 读取信息和写信息
- 用网络图片的url地址,从网络下载一张图片,并保存在本地硬盘的D盘的images2021文件夹,图片重命名为“IMAGE.jsp”(读网络信息,保存成文件,保存文件时使用字符流和字节流的方式)
import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; /** * @Author: Travelmate * @CreateTime: 2021/5/23 16:00 * @Description: */ public class Main { public static void main(String[] args) { // 图片网络资源的URL字符串 String urlStr = "https://gitee.com/fancoding/blog-image/raw/master/Image/20210523212903.jpg"; // 下载的图片保存到的本地位置 String filePath = "D:/images2021/"; // 图片重命名 String fileName = "523.jpg"; try { // 创建 URL 对象 URL url = new URL(urlStr); // 获取 URLConnection 对象 URLConnection conn = url.openConnection(); // 设置连接超时时间为 3秒 conn.setConnectTimeout(3 * 1000); // 获取网络输入流 InputStream inputStream = conn.getInputStream(); // 从网络输入流中读取字节数组(从网络下载的内容) byte[] buffer = new byte[1024]; int len = 0; // 字节数组输出流, 用来存储追加从网络输入流中读取的内容 ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 从输入流读取到buffer数组中 while((len = inputStream.read(buffer)) != -1){ bos.write(buffer, 0, len); } // 获取字节数组输出流中的字节数组, 该字节数组存储到本地 byte[] getData = bos.toByteArray(); bos.close(); // 创建文件保存位置, 即文件夹对象 File fileDir = new File(filePath); // 如果文件夹不存在 if (!fileDir.exists()) { // 创建文件夹 fileDir.mkdir(); } // 通过文件夹和文件名创建文件对象 File file = new File(fileDir, fileName); // 创建文件输出流, 用于写文件 FileOutputStream fos = new FileOutputStream(file); // 把之前从网络中下载的字节数组写入文件 fos.write(getData); if (fos != null) { fos.close(); } if (inputStream != null) { inputStream.close(); } System.out.println("tips: " + url + " 下载成功") ; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
- 发送信息给网络, 读网络信息。该例子包含两个程序,一个是客户端,用于连接服务器端,发送数据给服务器端,并接收服务器端的数据;服务端的作用是接收客户端发送的字符串,反序后,发送回客户端,即客户端发送出一个字符串,收到该字符串的反序。
客户端代码
import java.io.*; import java.net.URL; import java.net.URLConnection; import java.util.Scanner; /** * @Author: Travelmate * @CreateTime: 2021/5/23 16:00 * @Description: */ public class Main { public static void main(String[] args) throws Exception { Scanner sc = new Scanner(System.in); URL url = new URL("http://localhost:9098/2021523_war_exploded/"); // 创建 URL, 用于连接本地运行的一个Web应用程序 URLConnection conn = url.openConnection(); // 允许连接进行输出操作 conn.setDoOutput(true); // 通过 URLConnection 对象获取网络输出流, 用于向 URL 所代表的Web服务器发送数据 OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); out.write("string=" + sc.nextLine()); out.close(); // 通过 URLConnection 对象获取网络输入流, 用于接收Web服务器返回的数据 BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String decodeString; while ((decodeString = in.readLine()) != null) { System.out.println(decodeString); } in.close(); sc.close(); } }
服务器端:WEB项目(一个Servlet),
import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:01 * @Description: */ public class ReverseServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); String reverseStr = "no string"; // 获取客户端传过来的数据 String string = req.getParameter("string"); if (string != null) { // 反转字符串 reverseStr = new StringBuffer(string).reverse().toString(); } // 输出反转后的字符串 System.out.println(reverseStr); resp.setContentType("text/html"); resp.setCharacterEncoding("utf-8"); PrintWriter out = resp.getWriter(); // 返回到客户端 out.print(reverseStr); out.flush(); out.close(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
Tips:先运行服务端,再运行客户端
Socket 和 ServerSocket 的编程应用
-
基于TCP的单客户端/服务器端通信(服务器端如何绑定端口,监听连接,客户端如何连服务器,连上后双方如何通过Socket对象获取输入输出流进行通信)
- 客户端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:43 * @Description: 单客户端,在应用中后运行 */ public class Client { public static void main(String[] args) { String hostName = "";// 服务器的IP地址,这里表示本机,相当于127.0.0.1 int portNumber = 7; try{ // 客户端通过创建Socket,通过服务器的IP地址和端口号,连接服务器 Socket echoSocket = new Socket("127.0.0.1", 7); // 通过该Socket和服务器端通信,如果想向服务器发送消息,用Socket获取输出流 // 如果想接收服务器端的消息,用Socket获取输入流 PrintWriter out = new PrintWriter(echoSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream())); BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); String userInput; // 接收键盘传过来的数据 while ((userInput = stdIn.readLine()) != null) { // 按ctrl+z结束键盘输入 // 向服务器发送消息 out.println(userInput); // 接收服务器发过来的消息,并输出到屏幕 System.out.println("echo: " + in.readLine()); } } catch (UnknownHostException e) { e.printStackTrace(); System.exit(1); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } }
- 服务器端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:44 * @Description: 单服务器端,先运行 */ public class Server { public static void main(String[] args) { try { //创建服务器端套接字,打开端口,用于监听来自客户端的连接请求 ServerSocket serverSocket = new ServerSocket(7); //监听客户端的连接请求,如果监听到连接,则返回一个新的套接字 Socket clientSocket = serverSocket.accept(); //用新的套接字打开输出流 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); //用新套接字打开输入流 BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); Scanner scanner=new Scanner(System.in); String inputLine; while ((inputLine = in.readLine()) != null) { //从输入流接收客户端传过来的数据 System.out.println("来自客户端: " + inputLine); out.println(inputLine);//写入输出流中,即发送回客户端 } scanner.close(); } catch (IOException e) { e.printStackTrace(); } } }
运行时,先运行服务器端,再运行客户端,在客户端输入数据,发送给服务器,服务器接收后再返回给客户端,客户端接收后打印到屏幕。
客户端界面服务器端界面
-
基于TCP的多客户端/服务器端通信(实现客户端通过多线程收发消息)
- 客户端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:57 * @Description: 多客户端,在服务器端程序启动后再启动,可以多次启动,实现多个客户端连接同一个服务器 */ public class MultiClient { public MultiClient(){ //类的构造方法,主要执行代码在此方法中 String hostName = ""; int portNumber = 7; try { //用服务器的IP地址和端口号创建Socket,连接服务器 Socket echoSocket = new Socket(hostName, portNumber); //获取和服务器通信的Socket管道的输入流和输出流 PrintWriter out = new PrintWriter(echoSocket.getOutputStream(), true);//输出流,用于发送数据给服务器 BufferedReader in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));//输入流,用于接收服务器端传来的数据 BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); System.out.println("客户端:"+echoSocket.getLocalAddress().getHostName()+":" +echoSocket.getLocalPort());//输出当前客户端的IP地址和端口号 //发送信息的线程,用输出流out实现发数据到服务器 new Thread(new Runnable(){ //实现一个匿名Runnable类,创建匿名对象,并用该匿名对象创建Thread线程对象 @Override public void run() { String userInput; try { //从键盘接收数据 while (( userInput = stdIn.readLine()) != null) { //按ctrl+z结束键盘输入 //输出数据到服务器 out.println(userInput); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); System.exit(1); } } }).start();//线程对象调用start方法启动线程,自动执行Runnable对象中的run方法 //接收服务器的信息的线程,用in对象实现接收服务器端传过来的数据 new Thread(new Runnable(){ @Override public void run() { String receiveStr; try { //接收服务器发过来的数据 while ((receiveStr = in.readLine()) != null) { //把接收的数据输出到控制台屏幕 System.out.println("echo: " + receiveStr); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); System.exit(1); } } }).start(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { // TODO Auto-generated method stub new MultiClient(); } }
- 服务器端
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:59 * @Description: 服务器端,先于客户端启动,通过多线程实现接收多个客户端的连接 */ public class MultiServer { ServerSocket serverSocket; //clients用于存储连接到多个客户端的Socket List<Socket> clients = new ArrayList<Socket>(); public MultiServer() { //构造方法,主要执行代码在此方法中 try { //绑定端口,创建服务器端套接字。 serverSocket = new ServerSocket(7); while (true) { // 监听客户端连接,如果监听到,创建对应的新套接字 Socket clientSocket = serverSocket.accept(); // 把新套接字存储在列表中 clients.add(clientSocket); // 启动新的线程,把当前连接到客户端的新套接字和所有连接到客户端的套接字 //通过线程的构造方法的参数传递进线程, //在线程中实现与当前客户端的通信 new ServerThread(clientSocket, clients).start(); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new MultiServer(); } }
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; /** * @Author: Travelmate * @CreateTime: 2021/5/23 22:59 * @Description: 服务器端使用的线程 */ public class ServerThread extends Thread { Socket myClient;// 连接到当前客户端的Socket List<Socket> clients;// 连接到所有客户端的Socket String clientHostname;// 当前客户端的IP地址 int clientPort;// 当前客户端的端口号 // 构造方法,参数是连接当前客户端的Socket和连接到所有客户端的Socket列表 public ServerThread(Socket myClient, List<Socket> clients) { super(); this.myClient = myClient; this.clients = clients; } @Override public void run() { // 线程的执行主体 // 接收来自当前客户端的信息,转发给列表中的所有客户端 // 获取当前客户端的地址 InetSocketAddress clientAddress = (InetSocketAddress) myClient.getRemoteSocketAddress(); // 获取客户端IP地址 clientHostname = clientAddress.getHostName(); // 获取客户端的端口号 clientPort = clientAddress.getPort(); System.out.println("第" + clients.size() + "个客户端:" + clientHostname + ":" + clientPort + " 已连接"); try { String inStr; // 获取套接字输入流 BufferedReader in = new BufferedReader( new InputStreamReader(myClient.getInputStream())); // 用in对象接收来自当前客户端的信息 while ((inStr = in.readLine()) != null) { System.out.println(inStr + "。 来自" + clientHostname + ":" + clientPort + ""); // 用循环使用列表中的Socket把接收的信息再发出去(输出) for (Socket client : clients) { if (client != null) { PrintWriter out = new PrintWriter( client.getOutputStream(), true); out.println(inStr + "。 来自" + clientHostname + ":" + clientPort + ""); } } } } catch (IOException e) { // 当客户端关闭连接,服务器端无法发送接收数据发生异常 System.err.println(clientHostname + ":" + clientPort + "退出了。"); //e.printStackTrace(); } } }
DatagramPacket, DatagramSocket的编程应用
-
基于UDP的单客户端/服务器端通信(掌握服务器端如何绑定端口,客户端和服务器端如何通过DatagramSocket对象收发数据报(DatagramPacket对象))
- 客户端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.*; /** * @Author: Travelmate * @CreateTime: 2021/5/24 1:07 * @Description: 基于UDP协议的网络通信客户端,循环实现先发送一个数据报到服务器,再接收一个数据报 */ public class UdpClient { public static void main(String[] args) { DatagramSocket client = null; String sendStr; try { // 创建一个DatagramSocket,使用系统提供的端口 client = new DatagramSocket(); BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); // 通过键盘输入字符串,如果结束循环输入ctrl+z while ((sendStr = stdIn.readLine()) != null) { // 把键盘输入的字符串转成字节数组 byte[] buf = sendStr.getBytes(); // 把表示服务器端IP地址的字符串转换成 InetAddress 对象 InetAddress address = InetAddress.getByName("127.0.0.1"); // 使用要发送的字节数组,目标IP地址和端口号,构造数据报 DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4455); // 使用DatagramSocket对象发送数据报 client.send(packet); byte[] buf1 = new byte[256]; // 使用空字节数组构造空数据报 packet = new DatagramPacket(buf1, buf1.length); // 使用DatagramSocket对象接收数据到空数据报中 client.receive(packet); // 根据数据报的数据和长度构造字符串 String received = new String(packet.getData(), 0, packet.getLength()); // 显示接收到的字符串和远端的IP地址和端口号,trim方法表示删除字符串前后的空格 System.out.println("接收到的信息: " + received.trim() + " 来自" + packet.getAddress().getHostAddress() + ":" + packet.getPort()); } } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (client != null) client.close(); } } }
- 服务器端
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Scanner; /** * @Author: Travelmate * @CreateTime: 2021/5/24 1:08 * @Description: 基于UDP通信的服务器端,绑定端口4455 * 循环实现先接收一个数据报,再键盘输入一行字符串发送回去 */ public class UdpServer { public static void main(String[] args) { DatagramSocket server = null; Scanner scanner=null; try { //创建一个DatagramSocket对象 server = new DatagramSocket(4455); scanner=new Scanner(System.in); while (true) { byte[] buf = new byte[256]; // 创建用于接收数据的空数据报 DatagramPacket p = new DatagramPacket(buf, buf.length); server.receive(p);//用套接字接收数据包 //获取发送数据报的客户端的地址 InetAddress clientAddress = p.getAddress(); //获取发送数据报的客户端的端口号 int clientPort = p.getPort(); System.out.println( new String(p.getData()).trim() + " 来自:" + clientAddress.getHostAddress() + ":" + clientPort); byte[] buf2=scanner.nextLine().getBytes(); // 创建要发送的数据报(包含发送的数据,客户端地址和端口号),发送回客户端 DatagramPacket p2 = new DatagramPacket(buf2, buf2.length, clientAddress, clientPort); server.send(p2); } } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ if(server!=null)server.close(); if(scanner!=null)scanner.close(); } } }
客户端界面
服务器端界面
-
基于 UDP 的多客户端/服务器端通信(客户端通过多线程收发数据)