java.net.Socket类是Java完成客户端TCP操作的基础类。其他建立TCP网络连接的面向客户端的类如URL、URLConnection等最终都会调用这个类的方法。这个类本身使用原生代码与主机操作系统的本地TCP栈进行通信。
Java中的Socket类是对客户端Socket的抽象,另外还有服务端Socket,是ServerSocket类,我们这里会一起讲到。

客户端Socket

构造Socket

Socket表示连接,那么肯定要指定连接的ip和端口号,Socket提供了几种构造方法:
image.png
我们来看其中最容易理解的一个:

  1. public Socket(String host, int port)
  2. throws UnknownHostException, IOException
  3. {
  4. this(host != null ? new InetSocketAddress(host, port) :
  5. new InetSocketAddress(InetAddress.getByName(null), port),
  6. (SocketAddress) null, true);
  7. }

这个构造方法就是提供一个host和一个端口号,然后它调用的是另外一个构造方法,我们从这可以看出,它使用host和端口号初始化了InetSocketAddress,InetSocketAddress代表一个IP地址(包括ip地址和端口号)。我们来看下这个构造方法的逻辑:

  1. private Socket(SocketAddress address, SocketAddress localAddr,
  2. boolean stream) throws IOException {
  3. setImpl();
  4. // backward compatibility
  5. if (address == null)
  6. throw new NullPointerException();
  7. try {
  8. createImpl(stream);
  9. if (localAddr != null)
  10. bind(localAddr);
  11. connect(address);
  12. } catch (IOException | IllegalArgumentException | SecurityException e) {
  13. try {
  14. close();
  15. } catch (IOException ce) {
  16. e.addSuppressed(ce);
  17. }
  18. throw e;
  19. }
  20. }

它接收的第一个SocketAddress表示要连接的地址,第二个参数表示本地地址,也就是说我们可以指定从本地的哪个ip和哪个端口号去连接,不过一般情况下我们不需要指定。至于它的实现逻辑,就不去看了。

从Socket中读取数据

建立了Socket连接之后,我们就可以从Socket中获取数据了。这个很简单,因为数据就是一个输入流,我们可以直接通过它的getInputStream方法来得到这个输入流,然后解析其中的数据。
下面是一个示例,time.nist.gov这个host的13端口会返回一个可读的当前时间,我们可以用telnet来获取它:
image.png
如果通过socket来实现,也比较简单:

  1. public static void main(String[] args) throws IOException {
  2. Socket socket = new Socket("time.nist.gov",13);
  3. System.out.println(socket);
  4. InputStream inputStream = socket.getInputStream();
  5. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
  6. StringBuilder stringBuilder = new StringBuilder();
  7. int c;
  8. while((c=bufferedReader.read())!=-1){
  9. stringBuilder.append((char)c);
  10. }
  11. System.out.println(stringBuilder);
  12. }

只要获取socket,然后拿到输入流,然后解析其中的数据即可。

  1. 59697 22-04-28 11:41:17 50 0 0 305.5 UTC(NIST) *

注意我们使用socket的InputStream拿到的是TCP的数据:
image.png
也就是TCP包里面除了首部之外的数据。所以我们要想知道解析这个数据,就需要知道它携带的数据的格式,然后根据格式来正确解析它。例如,如果要发送和接收HTTP请求数据,从InputStream中获取到的数据就会包含HTTP的header和HTTP的用户数据。

写入Socket

除了读取数据,Socket同样支持写入数据,并且实现也很简单,只要获取OutputStream即可:

  1. public static void main(String[] args) throws IOException {
  2. try(Socket socket = new Socket("time.nist.gov",13)){
  3. OutputStream outputStream = socket.getOutputStream();
  4. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
  5. bufferedWriter.write("hello");
  6. }
  7. }

与读取数据一样,我们需要知道服务端需要怎样的数据格式,才能按照这种格式写入。

服务端Socket

客户端请求数据要被服务端处理,我们可以使用ServerSocket来构造一个服务端Socket来接收并处理客户端的请求。

构造服务端Socket

要想构造一个ServerSocket,最少得需要一个指定一个要绑定的端口。ServerSocket类提供了下面这些构造方法:
image.png
我们来看其中最简单的一个:

  1. public ServerSocket(int port) throws IOException {
  2. this(port, 50, null);
  3. }

一般来说,最简单的构造方法肯定是通过调用其他构造方法来实现的,这个也不例外,我们来看:

  1. public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
  2. setImpl();
  3. if (port < 0 || port > 0xFFFF)
  4. throw new IllegalArgumentException(
  5. "Port value out of range: " + port);
  6. if (backlog < 1)
  7. backlog = 50;
  8. try {
  9. bind(new InetSocketAddress(bindAddr, port), backlog);
  10. } catch(SecurityException e) {
  11. close();
  12. throw e;
  13. } catch(IOException e) {
  14. close();
  15. throw e;
  16. }
  17. }

这个构造方法有三个参数,第一个是要绑定的本地端口,如果是0,则会自动锁定一个端口,第二个是请求连接的最大数量,默认是50,第三个InetAddress表示从本地的哪个ip接收请求,如果为null,表示接收本地任意地址的请求。
理解了这个构造方法,其他的就简单了,因为其他构造方法都是调用这个方法来实现的。

接收请求

ServerSocket创建成功之后,我们需要使用它的accept方法来监听这个端口的入站连接。accept方法会一直阻塞,直到一个客户端尝试连接。此时accept方法将返回一个连接客户端和服务端的Socket对象,有了这个Socket,我们就可以获取它的InputStream和OutputStream来进行数据读取和写入了。