diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b8d7927 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# Java sources +*.java text diff=java +*.gradle text diff=java +*.gradle.kts text diff=java + +# These files are text and should be normalized (Convert crlf => lf) +*.css text diff=css linguist-language=java +*.df text +*.htm text diff=html +*.html text diff=html linguist-language=java +*.js text linguist-language=java +*.jsp text linguist-language=java +*.jspf text linguist-language=java +*.jspx text linguist-language=java +*.properties text +*.tld text linguist-language=java +*.tag text linguist-language=java +*.tagx text linguist-language=java +*.xml text linguist-language=java + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.class binary +*.dll binary +*.ear binary +*.jar binary +*.so binary +*.war binary +*.jks binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cbea2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# custom +target +.idea +*.iml +.settings +.project +.classpath diff --git a/README.md b/README.md new file mode 100644 index 0000000..27563e2 --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +# [Java Socket](https://github.com/happyflyer/Java-Socket) + +- [Java Socket 应用---通信是这样练成的](https://www.imooc.com/learn/161) + +## 1. 网络基础知识 + +Socket = IP 地址 + 端口号 + +**Socket** 是网络上运行的程序之间双向通信链路的终结点,是 TCP 和 UDP 的基础。 + +- ftp:`21` +- telent:`23` +- http:`80` +- ... + +针对网络通信的不同层次, Java 提供的网络功能的四大类: + +- `InetAddress`:用于标识网络上的硬件资源 +- `URL`:统一资源定位符,通过 URL 可以直接读取或写入网络上的资源 +- `Socket`:使用 TCP 协议实现网络通信的 Socket 相关的类 +- `Datagram`:使用 UDP 协议,数据保存数据报中,通过网络进行通信 + +## 2. InetAddress 类 + +```java +InetAddress address = InetAddress.getLocalHost(); +System.out.println(address.getHostName()); +System.out.println(address.getHostAddress()); +System.out.println(Arrays.toString(address.getAddress())); +System.out.println(address); +``` + +## 3. URL + +```java +try { + URL url = new URL("https://www.baidu.com"); + InputStream is = url.openStream(); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(isr); + String data = br.readLine(); + while (data != null) { + System.out.println(data); + data = br.readLine(); + } + br.close(); + isr.close(); + is.close(); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +## 4. Socket + +TCP 协议(传输控制协议)是面向连接、可靠的、有序的、以字节流发送数据。 + +Java 中基于 TCP 协议实现网络通信的类 + +- 客户端的 `Socket` 类 +- 服务端的 `ServerSocket` 类 + +![Socket通信模型](https://cdn.jsdelivr.net/gh/happyflyer/picture-bed@main/2021/Socket通信模型.gc1jqbig15s.jpg) + +Socket 通信实现步骤 + +1. 创建 `ServerSocket` 和 `Socket` +2. 打开连接到 `Socket` 的输入/输出流 +3. 按照协议对 `Socket` 进行读写操作 +4. 关闭输入/输出流,关闭 `Socket` + +### 4.1. 基于 TCP 的 Socket 通信 + +#### 4.1.1. 服务端 + +1. 创建 `ServerSocket` 对象,绑定监听端口 +2. 通过 `accept()` 方法监听客户端请求 +3. 连接建立后,通过输入流读取客户端发送的请求信息 +4. 通过输出流向客户端发送响应信息 +5. 关闭相应资源 + +```java +// 1. 创建 ServerSocket 对象,绑定监听端口 +ServerSocket serverSocket = new ServerSocket(8888); +System.out.println("***服务器已经启动,开始侦听客户端的连接***"); +// 2. 通过 accept() 方法监听客户端请求 +Socket socket = serverSocket.accept(); +// 3. 连接建立后,通过输入流读取客户端发送的请求信息 +InputStream is = socket.getInputStream(); +InputStreamReader isr = new InputStreamReader(is); +BufferedReader br = new BufferedReader(isr); +String info = null; +while ((info = br.readLine()) != null) { + System.out.println("我是服务器,客户端说:" + info); +} +socket.shutdownInput(); // 关闭输入流 +// 4. 通过输出流向客户端发送响应信息 +OutputStream os = socket.getOutputStream(); +PrintWriter pw = new PrintWriter(os); +pw.write("欢迎你"); +pw.flush(); +// 5. 关闭相应资源 +pw.close(); +os.close(); +br.close(); +isr.close(); +is.close(); +socket.close(); +serverSocket.close(); +``` + +#### 4.1.2. 客户端 + +1. 创建 `Socket` 对象,指明需要连接的服务的地址和端口号 +2. 连接建立后,通过输出流向服务发送请求信息 +3. 通过输入流获取服务响应的信息 +4. 关闭相应资源 + +```java +// 1. 创建 Socket 对象,指明需要连接的服务器的地址和端口号 +Socket socket = new Socket("127.0.0.1", 8888); +// 2. 连接建立后,通过输出流向服务器发送请求信息 +OutputStream os = socket.getOutputStream(); +PrintWriter pw = new PrintWriter(os); +pw.write("user:zhangsan;passwd:123456"); +pw.flush(); +socket.shutdownOutput(); // 关闭输出流 +// 3. 通过输入流获取服务器响应的信息 +InputStream is = socket.getInputStream(); +InputStreamReader isr = new InputStreamReader(is); +BufferedReader br = new BufferedReader(isr); +String resp = null; +while ((resp = br.readLine()) != null) { + System.out.println("我是客户端,服务器说:" + resp); +} +socket.shutdownInput(); // 关闭输入流 +// 4. 关闭相应资源 +pw.close(); +os.close(); +br.close(); +isr.close(); +is.close(); +socket.close(); +``` + +### 4.2. 使用多线程实现服务端与多客户端通信 + +1. 服务端创建 `ServerSocket` ,循环调用 `accept()` 等待客户端连接 +2. 客户端创建一个 `Socket` 并请求和服务端连接 +3. 服务端接受客户端请求,创建 `Socket` 与该客户端建立专线连接 +4. 建立连接的两个 `Socket` 在一个单独的 x 线程上对话 +5. 服务端继续等待新的连接 + +```java +// 1. 创建 ServerSocket 对象,绑定监听端口 +ServerSocket serverSocket = new ServerSocket(8888); +System.out.println("***服务器已经启动,开始侦听客户端的连接***"); +int count = 0; +// 2. 通过 accept() 方法监听客户端请求 +Socket socket; +while (true) { + socket = serverSocket.accept(); + ServerThread thread = new ServerThread(socket); + thread.setPriority(4); + thread.start(); + count++; + System.out.println("客户端的数量为:" + count); + InetAddress address = socket.getInetAddress(); + System.out.println("当前客户端的地址为:" + address.getHostAddress()); +} +``` + +## 5. UDP 编程 + +UDP 协议(用户数据报协议)是无连接、不可靠的、无序的。 + +UDP 协议传输速度快。 + +UDP 协议以**数据报**作为数据传输的载体。 + +使用 UDP 协议进行数据传输时, + +- 首先需要将要传输的数据定义成数据报(Datagram) +- 在数据报中指明数据所要达到的 Socket(主机和端口号) +- 然后再将数据报发送出去 + +- `DatagramPacket`:数据报包 +- `DatagramSocket`:进行端到端通信的类 + +### 5.1. 基于 UDP 的 Socket 通信 + +#### 5.1.1. 服务端 + +1. 创建 `DatagramSocket`,指定端口号 +2. 创建 `DatagramPacket` +3. 接受客户端发送的数据信息 +4. 读取数据 + +```java +// 1. 创建 DatagramSocket,指定端口号 +DatagramSocket socket = new DatagramSocket(8888); +// 2. 创建 DatagramPacket +byte[] data = new byte[1024]; +DatagramPacket packet = new DatagramPacket(data, data.length); +System.out.println("***服务器已经启动,开始侦听客户端的连接***"); +// 3. 接受客户端发送的数据信息 +// 此方法在接收到数据之前按一直会保持阻塞 +socket.receive(packet); +// 4. 读取数据 +String info = new String(data, 0, packet.getLength()); +System.out.println("我是服务器,客户端说:" + info); +// 服务器向客户端进行响应 +InetAddress address = packet.getAddress(); +int port = packet.getPort(); +byte[] resp = "你好,这是服务器对客户端的响应".getBytes(); +DatagramPacket respPacket = new DatagramPacket(resp, resp.length, address, port); +socket.send(respPacket); +socket.close(); +``` + +#### 5.1.2. 客户端 + +1. 定义发送信息 +2. 创建 `DatagramPacket`,包含所要发送的信息 +3. 创建 `DatagramSocket` +4. 发送数据 + +```java +// 1. 定义发送信息 +InetAddress address = InetAddress.getLocalHost(); +int port = 8888; +byte[] data = "user:zhangsan;passwd:888888".getBytes(); +// 2. 创建 DatagramPacket,包含所要发送的信息 +DatagramPacket packet = new DatagramPacket(data, data.length, address, port); +// 3. 创建 DatagramSocket +DatagramSocket socket = new DatagramSocket(); +// 4. 发送数据 +socket.send(packet); +// 客户端接收服务器端的响应 +byte[] resp = new byte[1024]; +DatagramPacket respPacket = new DatagramPacket(resp, resp.length); +socket.receive(respPacket); +String info = new String(resp, 0, respPacket.getLength()); +System.out.println("我是客户端,服务器响应的信息为:" + info); +``` + +## 6. 相关问题 + +1. 线程优先级 +2. 对于同一个 `Socket` + - 如果关闭了输出流,则与该输出流关联的 `Socket` 也会被关闭 + - 所以一般不需要关闭流,直接关闭 `Socket` 即可 +3. TCP 传输字符串、对象 +4. `Socket` 传递文件 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8a94562 --- /dev/null +++ b/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.example + Java-Socket + 0.1.0 + jar + + + UTF-8 + 11 + 11 + + + + + + junit + junit + 4.12 + test + + + + + \ No newline at end of file diff --git a/src/main/java/org/example/java_socket/address/InetAddressDemo.java b/src/main/java/org/example/java_socket/address/InetAddressDemo.java new file mode 100644 index 0000000..08eb4d5 --- /dev/null +++ b/src/main/java/org/example/java_socket/address/InetAddressDemo.java @@ -0,0 +1,20 @@ +package org.example.java_socket.address; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; + +/** + * InetAddress 类 + * + * @author lifei + */ +public class InetAddressDemo { + public static void main(String[] args) throws UnknownHostException { + InetAddress address = InetAddress.getLocalHost(); + System.out.println(address.getHostName()); + System.out.println(address.getHostAddress()); + System.out.println(Arrays.toString(address.getAddress())); + System.out.println(address); + } +} diff --git a/src/main/java/org/example/java_socket/socket/Client.java b/src/main/java/org/example/java_socket/socket/Client.java new file mode 100644 index 0000000..bfa6ad5 --- /dev/null +++ b/src/main/java/org/example/java_socket/socket/Client.java @@ -0,0 +1,42 @@ +package org.example.java_socket.socket; + +import java.io.*; +import java.net.Socket; + +/** + * 基于 TCP 的 Socket 通信 + * + * @author lifei + */ +public class Client { + public static void main(String[] args) { + try { + // 1. 创建 Socket 对象,指明需要连接的服务器的地址和端口号 + Socket socket = new Socket("127.0.0.1", 8888); + // 2. 连接建立后,通过输出流向服务器发送请求信息 + OutputStream os = socket.getOutputStream(); + PrintWriter pw = new PrintWriter(os); + pw.write("user:zhangsan;passwd:123456"); + pw.flush(); + socket.shutdownOutput(); // 关闭输出流 + // 3. 通过输入流获取服务器响应的信息 + InputStream is = socket.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String resp = null; + while ((resp = br.readLine()) != null) { + System.out.println("我是客户端,服务器说:" + resp); + } + socket.shutdownInput(); // 关闭输入流 + // 4. 关闭相应资源 + pw.close(); + os.close(); + br.close(); + isr.close(); + is.close(); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/example/java_socket/socket/Server.java b/src/main/java/org/example/java_socket/socket/Server.java new file mode 100644 index 0000000..b2a4446 --- /dev/null +++ b/src/main/java/org/example/java_socket/socket/Server.java @@ -0,0 +1,46 @@ +package org.example.java_socket.socket; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * 基于 TCP 的 Socket 通信 + * + * @author lifei + */ +public class Server { + public static void main(String[] args) { + try { + // 1. 创建 ServerSocket 对象,绑定监听端口 + ServerSocket serverSocket = new ServerSocket(8888); + System.out.println("***服务器已经启动,开始侦听客户端的连接***"); + // 2. 通过 accept() 方法监听客户端请求 + Socket socket = serverSocket.accept(); + // 3. 连接建立后,通过输入流读取客户端发送的请求信息 + InputStream is = socket.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String info = null; + while ((info = br.readLine()) != null) { + System.out.println("我是服务器,客户端说:" + info); + } + socket.shutdownInput(); // 关闭输入流 + // 4. 通过输出流向客户端发送响应信息 + OutputStream os = socket.getOutputStream(); + PrintWriter pw = new PrintWriter(os); + pw.write("欢迎你"); + pw.flush(); + // 5. 关闭相应资源 + pw.close(); + os.close(); + br.close(); + isr.close(); + is.close(); + socket.close(); + serverSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/example/java_socket/socket2/Client.java b/src/main/java/org/example/java_socket/socket2/Client.java new file mode 100644 index 0000000..2b3966c --- /dev/null +++ b/src/main/java/org/example/java_socket/socket2/Client.java @@ -0,0 +1,67 @@ +package org.example.java_socket.socket2; + +import java.io.*; +import java.net.Socket; + +/** + * 使用多线程实现服务端与多客户端通信 + * + * @author lifei + */ +public class Client { + public static void main(String[] args) { + Socket socket = null; + OutputStream os = null; + PrintWriter pw = null; + InputStream is = null; + InputStreamReader isr = null; + BufferedReader br = null; + try { + socket = new Socket("127.0.0.1", 8888); + os = socket.getOutputStream(); + pw = new PrintWriter(os); + pw.write("user:zhangsan;passwd:888888"); + pw.flush(); + socket.shutdownOutput(); // 关闭输出流 + is = socket.getInputStream(); + isr = new InputStreamReader(is); + br = new BufferedReader(isr); + String resp = null; + while ((resp = br.readLine()) != null) { + System.out.println("我是客户端,服务器说:" + resp); + } + socket.shutdownInput(); // 关闭输入流 + pw.close(); + os.close(); + br.close(); + isr.close(); + is.close(); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (pw != null) { + pw.close(); + } + if (os != null) { + os.close(); + } + if (br != null) { + br.close(); + } + if (isr != null) { + isr.close(); + } + if (is != null) { + is.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/org/example/java_socket/socket2/Server.java b/src/main/java/org/example/java_socket/socket2/Server.java new file mode 100644 index 0000000..952b253 --- /dev/null +++ b/src/main/java/org/example/java_socket/socket2/Server.java @@ -0,0 +1,36 @@ +package org.example.java_socket.socket2; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * 使用多线程实现服务端与多客户端通信 + * + * @author lifei + */ +public class Server { + public static void main(String[] args) { + try { + // 1. 创建 ServerSocket 对象,绑定监听端口 + ServerSocket serverSocket = new ServerSocket(8888); + System.out.println("***服务器已经启动,开始侦听客户端的连接***"); + int count = 0; + // 2. 通过 accept() 方法监听客户端请求 + Socket socket; + while (true) { + socket = serverSocket.accept(); + ServerThread thread = new ServerThread(socket); + thread.setPriority(4); + thread.start(); + count++; + System.out.println("客户端的数量为:" + count); + InetAddress address = socket.getInetAddress(); + System.out.println("当前客户端的地址为:" + address.getHostAddress()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/example/java_socket/socket2/ServerThread.java b/src/main/java/org/example/java_socket/socket2/ServerThread.java new file mode 100644 index 0000000..19d1623 --- /dev/null +++ b/src/main/java/org/example/java_socket/socket2/ServerThread.java @@ -0,0 +1,68 @@ +package org.example.java_socket.socket2; + +import java.io.*; +import java.net.Socket; + +/** + * 使用多线程实现服务端与多客户端通信 + * + * @author lifei + */ +public class ServerThread extends Thread { + private Socket socket; + + ServerThread(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + InputStream is = null; + InputStreamReader isr = null; + BufferedReader br = null; + OutputStream os = null; + PrintWriter pw = null; + try { + // 3. 连接建立后,通过输入流读取客户端发送的请求信息 + is = this.socket.getInputStream(); + isr = new InputStreamReader(is); + br = new BufferedReader(isr); + String info; + while ((info = br.readLine()) != null) { + System.out.println("我是服务器,客户端说:" + info); + } + this.socket.shutdownInput(); // 关闭输入流 + // 4. 通过输出流向客户端发送响应信息 + os = this.socket.getOutputStream(); + pw = new PrintWriter(os); + pw.write("欢迎你"); + pw.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + // 5. 关闭相应资源 + if (pw != null) { + pw.close(); + } + if (os != null) { + os.close(); + } + if (br != null) { + br.close(); + } + if (isr != null) { + isr.close(); + } + if (is != null) { + is.close(); + } + if (this.socket != null) { + this.socket.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/org/example/java_socket/udp/Client.java b/src/main/java/org/example/java_socket/udp/Client.java new file mode 100644 index 0000000..3223cb6 --- /dev/null +++ b/src/main/java/org/example/java_socket/udp/Client.java @@ -0,0 +1,32 @@ +package org.example.java_socket.udp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +/** + * 基于 UDP 的 Socket 通信 + * + * @author lifei + */ +public class Client { + public static void main(String[] args) throws IOException { + // 1. 定义发送信息 + InetAddress address = InetAddress.getLocalHost(); + int port = 8888; + byte[] data = "user:zhangsan;passwd:888888".getBytes(); + // 2. 创建 DatagramPacket,包含所要发送的信息 + DatagramPacket packet = new DatagramPacket(data, data.length, address, port); + // 3. 创建 DatagramSocket + DatagramSocket socket = new DatagramSocket(); + // 4. 发送数据 + socket.send(packet); + // 客户端接收服务器端的响应 + byte[] resp = new byte[1024]; + DatagramPacket respPacket = new DatagramPacket(resp, resp.length); + socket.receive(respPacket); + String info = new String(resp, 0, respPacket.getLength()); + System.out.println("我是客户端,服务器响应的信息为:" + info); + } +} diff --git a/src/main/java/org/example/java_socket/udp/Server.java b/src/main/java/org/example/java_socket/udp/Server.java new file mode 100644 index 0000000..8688ea2 --- /dev/null +++ b/src/main/java/org/example/java_socket/udp/Server.java @@ -0,0 +1,35 @@ +package org.example.java_socket.udp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +/** + * 基于 UDP 的 Socket 通信 + * + * @author lifei + */ +public class Server { + public static void main(String[] args) throws IOException { + // 1. 创建 DatagramSocket,指定端口号 + DatagramSocket socket = new DatagramSocket(8888); + // 2. 创建 DatagramPacket + byte[] data = new byte[1024]; + DatagramPacket packet = new DatagramPacket(data, data.length); + System.out.println("***服务器已经启动,开始侦听客户端的连接***"); + // 3. 接受客户端发送的数据信息 + // 此方法在接收到数据之前按一直会保持阻塞 + socket.receive(packet); + // 4. 读取数据 + String info = new String(data, 0, packet.getLength()); + System.out.println("我是服务器,客户端说:" + info); + // 服务器向客户端进行响应 + InetAddress address = packet.getAddress(); + int port = packet.getPort(); + byte[] resp = "你好,这是服务器对客户端的响应".getBytes(); + DatagramPacket respPacket = new DatagramPacket(resp, resp.length, address, port); + socket.send(respPacket); + socket.close(); + } +} diff --git a/src/main/java/org/example/java_socket/url/UrlDemo.java b/src/main/java/org/example/java_socket/url/UrlDemo.java new file mode 100644 index 0000000..7ba2985 --- /dev/null +++ b/src/main/java/org/example/java_socket/url/UrlDemo.java @@ -0,0 +1,34 @@ +package org.example.java_socket.url; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * URL 类 + * + * @author lifei + */ +public class UrlDemo { + public static void main(String[] args) { + try { + URL url = new URL("https://www.baidu.com"); + InputStream is = url.openStream(); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(isr); + String data = br.readLine(); + while (data != null) { + System.out.println(data); + data = br.readLine(); + } + br.close(); + isr.close(); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/org/example/java_socket/.gitkeep b/src/test/java/org/example/java_socket/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29