前言
Socket是指网络上端到端的通信机制,一般基于传输层协议。本文以Java为例,专门讨论TCP协议的Socket,UDP协议的Socket也可以参考。
饥饿-读
考虑这样一个情况:一个简单的HTTP客户端访问服务器资源的程序在运行了一段时间后,莫名的停住了;或者是一个简单的JDBC查询数据库记录的程序在运行了一段时间后,没有动静了。此时,dump下Java目前的运行状态,基本上能看到如下代码片段:
Thread %d: (state = IN_NATIVE)
- java.net.SocketInputStream.socketRead0(java.io.FileDescriptor, byte[], int, int, int) @bci=0 (Compiled frame; information may be imprecise)
- java.net.SocketInputStream.read(byte[], int, int, int) @bci=87 (Compiled frame)
- java.net.SocketInputStream.read(byte[], int, int) @bci=11 (Compiled frame)
- java.net.SocketInputStream.read(byte[]) @bci=5 (Compiled frame)
- ... ... ...
多次dump发现,进程一直阻塞在socketRead0函数,也就是说进程已经「饿死」了。至于为什么会「饥饿」到死,这个原因就复杂了,可能是服务端自己产生了死锁,也可能是服务端运行异常,跳过了往客户端发送数据的代码。但是作为客户端,读不到数据可能不是最糟糕的,最糟糕的是阻塞导致其它代码不能执行。
这就需要打破这种阻塞状态。Java的Socket API 提供了这个机制。
void setSoTimeout(int timeout)
Socket的setSoTimeout方法,用于设置读取超时,单位是毫秒。默认情况下,值为0,即无限等待直到读取数据。如果设置为一个合理的值,比如30000,则读取超时为30秒,即30秒之后,如果数据还未到达,则抛出一个超时异常。
HTTP和JDBC底层都是基于TCP的Socket机制实现,所以不管是HTTP客户端,还是JDBC客户端,一般都会提供这个参数的配置。
死锁-写
死锁-写有一个经典的场景。
Server端 ↓
public class Server {
public static void main(String[] args) throws Exception {
int count = 0;
ServerSocket ss = new ServerSocket(6060);
Socket s = ss.accept();
while (true) {
s.getOutputStream().write(new byte[1024 * 8]);
s.getInputStream().read();
System.out.println("server: " + ++count);
}
}
}
Client端 ↓
public class Client {
public static void main(String[] args) throws Exception {
int count = 0;
Socket s = new Socket("localhost", 6060);
while (true) {
s.getOutputStream().write(new byte[1024 * 8]);
s.getInputStream().read();
System.out.println("client: " + ++count);
}
}
}
两个程序运行几次后,就不再打印任何消息了,即两个进程处于死锁状态了。造成死锁的原因自然是满足了死锁的四个必要条件:
- 互斥;Server端和Client端都既是生产者,也是消费者,互斥的使用两个TCP缓冲队列。
- 请求与保持;由于write方法每次写8KB,而read方法每次只读1B,所以两个缓冲队列都很快被填满。此时,两端都请求空的缓冲区,并保持着对方空缓冲的释放权利。
- 不剥夺;这两个程序请求资源是非抢占式的,显然不会被剥夺。
- 环路等待;Server端和Client端的程序都阻塞在write,互相等待对方释放空的缓冲区。
解决死锁的办法也很简单,将write和read分离在不同的线程,写归写,读归读,打破环路等待的条件。那样虽然读的比较慢,但死锁是不存了。
将读写分离成不同线程时,需要特别注意:不要在Socket对象上加同步锁。这样跟不分离没有实质上的区别。
结论
Socket编程一般都是多进程编程(服务端和客户端),多线程编程(Socket发送接收一般都是独立线程)的综合。所以,多进程、多线程的各种编程问题层出不穷,最突出的就是饥饿和死锁,需要足够多的代码来沉淀。