socket 起源于UNIX,在 Unix 一切皆文件哲学的思想下,socket 是一种"打开---读/写---关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

连接服务器

Telnet 命令

telnet 是一种用于网络编程的非常强大的调试工具。可以使用其来连接远程计算机,或者用于其它网络服务进行通信测试。

获取当前的UTC时间

telnet time-a.nist.gov 13 

响应内容

59760 22-06-30 09:02:19 50 0 0 946.4 UTC(NIST) * 

Java 连接

在 Java 中,可以使用 Socket(套接字)进行连接到某个地址的端口,并打印响应内容。Socket 负责启动该程序内部与外部之间的通信。

获取当前的UTC时间

public class SocketTest { public static void main(String[] args) { try (Socket socket = new Socket("time-a.nist.gov", 13); Scanner scanner = new Scanner(socket.getInputStream(), "UTF-8")) { while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } } catch (Exception e) { e.printStackTrace(); } } } 

程序打印内容

59760 22-06-30 09:50:51 50 0 0 787.6 UTC(NIST) * 

?程序说明

  • 打开一个套接字 `Socket socket = new Socket(“time-a.nist.gov”, 13)`

  • 使用Scanner类接收套接字的 InputStream,方便后续打印在控制台 `Scanner scanner = new Scanner(socket.getInputStream(), “UTF-8”)`

Socket 类非常简单易用,因为 Java 库隐藏了建立网络连接和通过连接发送数据的复杂过程。实际上, java.net 包提供的编程接口与操作文件时所使用的接口基本相同。

连接超时

使用 Socket 连接主机,在没有响应信息可供读取时,读操作(read())都将会被阻塞。

场景一

如果连接的主机不可访问,那么程序将会阻塞很长时间,受操作系统限制最终导致此次连接超时。

解决办法:通过先构造一个无连接的 Socket,再对这个 Socket 进行设置主机,端口,超时时间等内容。

Socket s = new Socket(); s. connect(new InetSocketAddress (host, port) , timeout); 

场景二

基于 Socket 下载文件时,如果文件过大,程序将会处于长时间阻塞状态,这显然是不合理的,可以通过 `setSoTimeout` 方法,设置读取超时时间。

s.setSoTimeout(2000); 

IP

网址和 IP 地址之间进行转换,使用 InetAddress 类。

获取百度IP

InetAddress address = InetAddress.getByName("www.baidu.com"); 

单个网址,对应多个IP地址。

InetAddress[] address = InetAddress.getAllByName("www.baidu.com"); 

本地主机IP

InetAddress.getLocalHost().getHostAddress() 

创建服务器

使用ServerSocket类创建服务器套接字,并监听8189端口。

ServerSocket serverSocket = new ServerSocket(8189); Socket socket = serverSocket.accept(); 

如果有客户端连接到了8189端口,可通过输出/输出流进行内容的交互。

服务器接收客户端内容,使用输入流。

服务器发送内容至客户端,使用输出流。

InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream() 

单用户服务器实现

A用户连接到了服务器,B用户则需等到A用户断开连接。

服务端代码

public class ServerSocketTest { public static void main(String[] args) throws IOException { try (ServerSocket serverSocket = new ServerSocket(8189); Socket socket = serverSocket.accept(); InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(inputStream, "UTF-8"); PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true); printWriter.println("Hello World! Enable c to exist;"); boolean done = false; while (!done && scanner.hasNext()) { String line = scanner.nextLine().trim(); System.out.println("Echo:" + line); if (line.equalsIgnoreCase("c")) { done = true; } } } } } 

服务端输出

Echo:c 

客户端输出

$ telnet 127.0.0.1 8189 Hello World! Enable c to exist; c 遗失对主机的连接 

多用户服务实现

在单用户的代码中,`accept()`方法作用是从连接中取出一个客户端的连接。

那么可以通过线程的方式,每次有客户端连接服务器,都启用一个新的线程去处理客户端与服务器之间的交互,而主线程,则立即返回并等到下一个连接。这样就实现了多用户。

服务端代码

public class ServerSocketTest { public static void main(String[] args) throws IOException { try (ServerSocket serverSocket = new ServerSocket(8189)) { int count = 1; while (true) { Socket socket = serverSocket.accept(); System.out.println("count:" + count); Thread thread = new Thread(() -> { try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(inputStream, "UTF-8"); PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true); printWriter.println("Hello World! Enable c to exist;"); boolean done = false; while (!done && scanner.hasNext()) { String line = scanner.nextLine().trim(); System.out.println("Thread Id:" + Thread.currentThread().getId() + " Echo:" + line); if (line.equalsIgnoreCase("c")) { done = true; } } } catch (IOException e) { e.printStackTrace(); } }); thread.start(); count++; } } } } 

客户端1号

$ telnet 127.0.0.1 8189 Hello World! Enable c to exist; c 遗失对主机的连接。 

客户端2号

$ telnet 127.0.0.1 8189 Hello World! Enable c to exist; c 遗失对主机的连接。 

服务端输出

count:1 count:2 Thread Id:12 Echo:c Thread Id:13 Echo:c 

半关闭

半关闭( half-close ):Socket 连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据。

服务器接收客户端传送的数据,服务器使用输入流接收客户端数据,而当数据传输完成后,服务器关闭输出流。此时的服务器输入流还是打开的,客户端仍可以向服务器传送数据。

半连接只适用于一站式( one-shot )的服务,例如 HTTP 服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接

void shutdownOutput()

将输出流设为"流结束"

void shutdownlnput()

将输入流设为"流结束"

boolean isOutputShutdown()

如果输出已被关闭,则返回 true

boolean isInputShutdown ()

如果输入已被关闭,则返回 true

可中断Socket

客户端,连接到一个Socket时,当前客户端的线程会被阻塞,直到连接建立成功或者超时。

客户端,通过Socket读写数据时候,当前客户端线程也会被阻塞,直到操作成功或者超时。

当客户端因服务端无法响应数据,导致当前线程被阻塞,并且无法通过 interrupt() 方法解除阻塞状态时,需要用到SocketChannel类来解决。

打开Socket通道

SocketChannel channel = SocketChannel.open(new InetSocketAddress (host, port)); 

Scanner 从 SocketChannel 读取数据

Scanner in = new Scanner(sock.getInputStream(), "UTF-8"); 

举个例子

try (SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8189))) { in = new Scanner(channel, "UTF-8"); // 线程中断,不执行while循环,关闭SocketChanel while (!Thread.currentThread().isInterrupted()) { messages.append("Reading "); if (in.hasNextLine()) { String line = in.nextLine(); messages.append(line); messages.append("\n"); } } } 

完整代码

import javax.swing.*; import java.awt.*; import java.io.*; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.SocketChannel; import java.util.Scanner; public class InterruptibleSocketTest { public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new InterruptibleSocketFrame(); frame.setTitle("InterruptibleSocketTest"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } } class InterruptibleSocketFrame extends JFrame { private Scanner in; private JButton interruptibleButton; private JButton blockingButton; private JButton cancelButton; private JTextArea messages; private TestServer server; private Thread connectThread; public InterruptibleSocketFrame() { JPanel northPanel = new JPanel(); add(northPanel, BorderLayout.NORTH); final int TEXT_ROWS = 20; final int TEXT_COLUMNS = 60; messages = new JTextArea(TEXT_ROWS, TEXT_COLUMNS); add(new JScrollPane(messages)); interruptibleButton = new JButton("Interruptible"); blockingButton = new JButton("Blocking"); northPanel.add(interruptibleButton); northPanel.add(blockingButton); interruptibleButton.addActionListener(event -> { interruptibleButton.setEnabled(false); blockingButton.setEnabled(false); cancelButton.setEnabled(true); connectThread = new Thread(() -> { try { connectInterruptibly(); } catch (IOException e) { messages.append("\nInterruptibleSocketTest.connectInterruptibly: " + e); } }); connectThread.start(); }); blockingButton.addActionListener(event -> { interruptibleButton.setEnabled(false); blockingButton.setEnabled(false); cancelButton.setEnabled(true); connectThread = new Thread(() -> { try { connectBlocking(); } catch (IOException e) { messages.append("\nInterruptibleSocketTest.connectBlocking: " + e); } }); connectThread.start(); }); cancelButton = new JButton("Cancel"); cancelButton.setEnabled(false); northPanel.add(cancelButton); cancelButton.addActionListener(event -> { connectThread.interrupt(); cancelButton.setEnabled(false); }); server = new TestServer(); new Thread(server).start(); pack(); } /** * Connects to the test server, using interruptible I/O */ public void connectInterruptibly() throws IOException { messages.append("Interruptible:\n"); try (SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8189))) { in = new Scanner(channel, "UTF-8"); while (!Thread.currentThread().isInterrupted()) { messages.append("Reading "); if (in.hasNextLine()) { String line = in.nextLine(); messages.append(line); messages.append("\n"); } } } finally { EventQueue.invokeLater(() -> { messages.append("Channel closed\n"); interruptibleButton.setEnabled(true); blockingButton.setEnabled(true); }); } } /** * Connects to the test server, using blocking I/O */ public void connectBlocking() throws IOException { messages.append("Blocking:\n"); try (Socket sock = new Socket("localhost", 8189)) { in = new Scanner(sock.getInputStream(), "UTF-8"); while (!Thread.currentThread().isInterrupted()) { messages.append("Reading "); if (in.hasNextLine()) { String line = in.nextLine(); messages.append(line); messages.append("\n"); } } } finally { EventQueue.invokeLater(() -> { messages.append("Socket closed\n"); interruptibleButton.setEnabled(true); blockingButton.setEnabled(true); }); } } /** * A multithreaded server that listens to port 8189 and sends numbers to the client, simulating * a hanging server after 10 numbers. */ class TestServer implements Runnable { public void run() { try (ServerSocket s = new ServerSocket(8189)) { while (true) { Socket incoming = s.accept(); Runnable r = new TestServerHandler(incoming); Thread t = new Thread(r); t.start(); } } catch (IOException e) { messages.append("\nTestServer.run: " + e); } } } /** * This class handles the client input for one server socket connection. */ class TestServerHandler implements Runnable { private Socket incoming; private int counter; /** * Constructs a handler. * * @param i the incoming socket */ public TestServerHandler(Socket i) { incoming = i; } public void run() { try { try { OutputStream outStream = incoming.getOutputStream(); PrintWriter out = new PrintWriter( new OutputStreamWriter(outStream, "UTF-8"), true /* autoFlush */); while (counter < 100) { counter++; if (counter <= 10) out.println(counter); Thread.sleep(100); } } finally { incoming.close(); messages.append("Closing server\n"); } } catch (Exception e) { messages.append("\nTestServerHandler.run: " + e); } } } } 

结果对比

可中断不可中断
客户端第10个数字打印之后,服务器线程进入睡眠,点击 Cancel 按钮,当前客户端线程取消阻塞状态,当前通道关闭,服务器关闭。客户端第10个数字打印之后,服务器线程进入睡眠,点击 Cancel 按钮,当前客户端线程无法取消阻塞状态,需等服务器关闭连接。

URL和URI

统一资源定位符( Uniform Resource Locator, URL )和统一资源标识符 (Uniform Resource Identifier, URI)。

URI 是个纯粹的语法结构,包含用来指定 Web 资源的字符串的各种组成部分。URL 是 URI 的一个特例,它包含了用于定位 Web 资源的信息。

其他 URI,比如:mailto:2964556627@qq.com。像这样的 URI,称为:URN (uniform resource name ,统一资源名称)。