Java高级程序设计

网络编程

Networking

Computers running on the Internet communicate to each other using either the Transmission Control Protocol (TCP) or the User Datagram Protocol (UDP).

When you write Java programs that communicate over the network, you are programming at the application layer.

https://docs.oracle.com/javase/tutorial/networking/overview/networking.html

基本概念

  • TCP (Transmission Control Protocol) is a connection-based protocol that provides a reliable flow of data between two computers.
  • UDP (User Datagram Protocol) is a protocol that sends independent packets of data, called datagrams, from one computer to another with no guarantees about arrival.
  • The TCP and UDP protocols use ports to map incoming data to a particular process running on a computer.
  • The Hypertext Transfer Protocol (HTTP), File Transfer Protocol (FTP), and Telnet are all examples of applications that require a reliable communication channel.

URL

URL is an acronym for Uniform Resource Locator and is a reference (an address) to a resource on the Internet. For instance: http://example.com

  • Protocol identifier
  • Resource name
    • Host Name
    • Filename
    • Port Number
    • Reference

Reading Directly from a URL

public static void main(String[] args) throws Exception {
    URL oracle = new URL("http://www.oracle.com/");
    BufferedReader in = new BufferedReader(
        new InputStreamReader(oracle.openStream()));

    String inputLine;
    while ((inputLine = in.readLine()) != null)
        System.out.println(inputLine);
    in.close();
}

Connecting to a URL

try {
    URL myURL = new URL("http://example.com/");
    URLConnection myURLConnection = myURL.openConnection();
    myURLConnection.connect();
} 
catch (MalformedURLException e) { 
    // new URL() failed
    // ...
} 
catch (IOException e) {   
    // openConnection() failed
    // ...
}

Reading from a URLConnection

public static void main(String[] args) throws Exception {
    URL oracle = new URL("http://www.oracle.com/");
    URLConnection yc = oracle.openConnection();
    BufferedReader in = new BufferedReader(new InputStreamReader(
                                yc.getInputStream()));
    String inputLine;
    while ((inputLine = in.readLine()) != null) 
        System.out.println(inputLine);
    in.close();
}

Writing to a URLConnection

URL url = new URL("http://example.com/servlet/ReverseServlet");
URLConnection connection = url.openConnection();
connection.setDoOutput(true);

OutputStreamWriter out = new OutputStreamWriter( connection.getOutputStream());
out.write("string=" + stringToReverse);
out.close();

BufferedReader in = new BufferedReader( new InputStreamReader(connection.getInputStream()));
String decodedString;
while ((decodedString = in.readLine()) != null) {
    System.out.println(decodedString);
}
in.close();

Sockets

URLs and URLConnections provide a relatively high-level mechanism for accessing resources on the Internet. Sometimes your programs require lower-level network communication, for example, when you want to write a client-server application.

To communicate over TCP, a client program and a server program establish a connection to one another. Each program binds a socket to its end of the connection. To communicate, the client and the server each reads from and writes to the socket bound to the connection.

Socket

A socket is one endpoint of a two-way communication link between two programs running on the network. A socket is bound to a port number so that the TCP layer can identify the application that data is destined to be sent to.

Server listen

A server runs on a specific computer and has a socket that is bound to a specific port number.

The server just waits, listening to the socket for a client to make a connection request.

ServerSocket serverSocket = new ServerSocket(80);

Client connect

The client knows the hostname of the machine on which the server is running and the port number on which the server is listening. The client also needs to identify itself to the server so it binds to a local port number that it will use during this connection.

Socket echoSocket = new Socket(hostName, portNumber);

Server accept

If everything goes well, the server accepts the connection. Upon acceptance, the server gets a new socket bound to the same local port and also has its remote endpoint set to the address and port of the client. It needs a new socket so that it can continue to listen to the original socket for connection requests while tending to the needs of the connected client.

Socket clientSocket = serverSocket.accept();     

Client-server communication

The client and server can now communicate by writing to or reading from their sockets.

Socket选项

TCP_NODELAY

禁用Nagle算法,减少延迟。

Socket socket = new Socket("localhost", 8080);
socket.setTcpNoDelay(true);  // 禁用Nagle算法

Nagle算法:将多个小数据包合并发送,减少网络开销
适用场景:实时性要求高的应用(如游戏、实时通信)

SO_REUSEADDR

允许端口复用,解决TIME_WAIT状态下的端口占用问题。

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);  // 允许端口复用
serverSocket.bind(new InetSocketAddress(8080));

作用:允许在TIME_WAIT状态下重新绑定端口
适用场景:服务器重启时快速恢复

SO_KEEPALIVE

启用TCP保活机制,检测连接是否存活。

Socket socket = new Socket("localhost", 8080);
socket.setKeepAlive(true);  // 启用保活机制

保活机制

  • 默认2小时无数据后发送探测包
  • 探测包失败后关闭连接
  • 可检测到网络中断、主机崩溃等情况

SO_TIMEOUT

设置Socket读取超时时间。

Socket socket = new Socket("localhost", 8080);
socket.setSoTimeout(5000);  // 5秒超时

超时类型

  • 连接超时connect()操作的超时
  • 读取超时read()操作的超时
  • 写入超时:通常不超时,由操作系统控制

缓冲区大小设置

Socket socket = new Socket("localhost", 8080);
socket.setReceiveBufferSize(64 * 1024);  // 64KB接收缓冲区
socket.setSendBufferSize(64 * 1024);     // 64KB发送缓冲区

缓冲区大小

  • 默认值:通常8KB(系统相关)
  • 建议值:根据网络延迟和带宽调整
  • 过大:浪费内存,增加延迟
  • 过小:频繁系统调用,降低性能

例子

https://docs.oracle.com/javase/tutorial/networking/sockets/examples/EchoClient.java

https://docs.oracle.com/javase/tutorial/networking/sockets/examples/EchoServer.java

https://tools.ietf.org/html/rfc862

多个客户端

But it's still blocking! And too many threads cause performance issues!

Thread Context Switch

https://en.wikipedia.org/wiki/Context_switch

http://tutorials.jenkov.com/java-concurrency/costs.html

https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/

https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html

Non-blocking I/O

With non-blocking I/O, we can use a single thread to handle multiple concurrent connections.

  • Buffer
  • Channel
  • Selector

Selector

Java NIO has a class called "Selector" that allows a single thread to examine I/O events on multiple channels. That is, this selector can check the readiness of a channel for operations, such as reading and writing.

Selector底层实现:操作系统IO多路复用

Java NIO的Selector底层使用操作系统提供的IO多路复用机制:

操作系统 底层实现 特点
Linux epoll 高效,支持大量连接
macOS/BSD kqueue 高效,事件驱动
Windows select 传统方式,效率较低
Solaris /dev/poll 高效

epoll工作原理

// Java NIO Selector
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);

// 底层调用
// epoll_create() - 创建epoll实例
// epoll_ctl() - 注册/修改/删除事件
// epoll_wait() - 等待事件就绪
  • O(1)时间复杂度:事件就绪时直接通知,无需遍历
  • 支持大量连接:不受文件描述符限制
  • 边缘触发模式:减少系统调用次数

Reactor模式

单线程Reactor

Reactor线程
├── 监听连接事件
├── 分发事件到Handler
└── 处理IO读写

Handler
├── 处理连接
├── 处理读取
└── 处理写入

Reactor Tutorial

https://medium.com/coderscorner/tale-of-client-server-and-socket-a6ef54a74763

https://github.com/arukshani/JavaIOAndNIO

Reactor模式

核心组件

  • Selector:事件分发器,监听多个Channel
  • ServerSocketChannel:监听连接事件
  • SocketChannel:处理IO读写事件
  • SelectionKey:事件类型(ACCEPT、READ、WRITE)

多线程Reactor

Main Reactor
├── 监听连接事件
└── 分发到Sub Reactor

Sub Reactor (多个)
├── 处理IO读写
└── 分发到Worker线程池

Worker线程池
└── 处理业务逻辑

零拷贝技术

传统IO的数据拷贝

用户空间
  ↓ read()
内核空间
  ↓ DMA
网卡缓冲区
  ↓ 数据拷贝
内核缓冲区
  ↓ 数据拷贝
用户缓冲区

问题:多次数据拷贝,CPU开销大

零拷贝技术

1. sendfile(Linux)

// Java NIO中零拷贝
FileChannel sourceChannel = new FileInputStream("file.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));

// 零拷贝:直接在内核空间传输
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);
  • 减少数据拷贝次数
  • 减少CPU开销
  • 提高传输效率

2. mmap(内存映射)

// 内存映射文件
FileChannel channel = new RandomAccessFile("file.txt", "rw").getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());

// 直接操作内存,无需系统调用
buffer.put(data);
  • 直接操作内存,无需系统调用
  • 减少数据拷贝
  • 适合大文件传输

网络性能优化 - 连接管理

public class ConnectionPool {
    private Queue<Socket> pool = new ConcurrentLinkedQueue<>();
    
    public Socket getConnection() {
        Socket socket = pool.poll();
        if (socket == null || socket.isClosed()) {
            socket = new Socket("localhost", 8080);
        }
        return socket;
    }
    
    public void returnConnection(Socket socket) {
        if (socket.isConnected() && !socket.isClosed()) {
            pool.offer(socket);
        }
    }
}

连接复用,减少建立连接的开销;限制连接数,避免资源耗尽

批量操作

// 批量写入
List<String> messages = Arrays.asList("msg1", "msg2", "msg3");
StringBuilder batch = new StringBuilder();
for (String msg : messages) {
    batch.append(msg).append("\n");
}
socket.getOutputStream().write(batch.toString().getBytes());

// 批量读取
byte[] buffer = new byte[8192];
int bytesRead = socket.getInputStream().read(buffer);
  • 减少系统调用次数
  • 减少网络往返次数,提高吞吐量

缓冲区优化

// 使用缓冲流
BufferedInputStream bis = new BufferedInputStream(
    socket.getInputStream(), 8192);  // 8KB缓冲区
BufferedOutputStream bos = new BufferedOutputStream(
    socket.getOutputStream(), 8192);

// 使用NIO Buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);  // 直接内存
  • 减少系统调用
  • 提高IO效率
  • 直接内存减少拷贝