JavaSocket编程基础
1、BIO的基本模型图
上图描述了BIOSocket通信时的基本结构
2、Socket通信基本示例
服务端代码:
public class BioServer {
public static void main(String[] args) {
int port = 8080;
try {
ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();
new Thread(new ServerHandler(socket)).start();
}catch (Exception e){
e.printStackTrace();
}finally {
//做相应的结尾动作
}
}
static class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try{
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
String expression;
String result;
while(true){
//通过BufferedReader读取一行
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就尝试计算结果并返回
if((expression = in.readLine())==null) break;
System.out.println("服务器收到消息:" + expression);
try{
result = "返回"+expression;
}catch(Exception e){
result = "计算错误:" + e.getMessage();
}
out.println(result);
}
}catch(Exception e){
e.printStackTrace();
}finally{
//一些必要的清理工作,由于代码篇幅长,此处就不贴了
}
}
}
}
客户端代码:
public class Client {
//默认的端口号
private static int DEFAULT_SERVER_PORT = 12345;
private static String DEFAULT_SERVER_IP = "127.0.0.1";
public static void send(String expression){
send(DEFAULT_SERVER_PORT,expression);
}
public static void send(int port,String expression){
System.out.println("算术表达式为:" + expression);
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try{
socket = new Socket(DEFAULT_SERVER_IP,port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
out.println(expression);
System.out.println("___结果为:" + in.readLine());
}catch(Exception e){
e.printStackTrace();
}finally{
//一下必要的清理工作
}
}
}
3、Socket相关的网络知识
- int TCP_NODELAY = 0x0001:对此连接禁用 Nagle 算法。
- int SO_BINDADDR = 0x000F:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
- int SO_REUSEADDR = 0x04:设置套接字的 SO_REUSEADDR。
- int SO_BROADCAST = 0x0020:此选项启用和禁用发送广播消息的处理能力。
- int IP_MULTICAST_IF = 0x10:设置用于发送多播包的传出接口。
- int IP_MULTICAST_IF2 = 0x1f:设置用于发送多播包的传出接口。
- int IP_MULTICAST_LOOP = 0x12:此选项启用或禁用多播数据报的本地回送。
- int IP_TOS = 0x3:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
- int SO_LINGER = 0x0080:指定关闭时逗留的超时值。
- int SO_TIMEOUT = 0x1006:设置阻塞 Socket 操作的超时值: ServerSocket.accept(); SocketInputStream.read(); DatagramSocket.receive(); 选项必须在进入阻塞操作前设置才能生效。
- int SO_SNDBUF = 0x1001:设置传出网络 I/O 的平台所使用的基础缓冲区大小的提示。
- int SO_RCVBUF = 0x1002:设置传入网络 I/O 的平台所使用基础缓冲区的大小的提示。
- int SO_KEEPALIVE = 0x0008:为 TCP 套接字设置 keepalive 选项时
- int SO_OOBINLINE = 0x1003:置 OOBINLINE 选项时,在套接字上接收的所有 TCP 紧急数据都将通过套接字输入流接收。
3.1读超时设置SO_TIMEOUT:
当客户端发生了异常,但是客户端的连接并没有关闭,此时服务器应该关闭该socket以便节省资源,所以应该设置一个读取的超时时间,当超过指定的时间后,还没有读到数据,就假定这个连接已经失效了,然后抛异常,服务端关闭连接
3.2连接超时SO_TIMEOUT:
连接超时是在连接的适合等待的超时时间
3.3判断Socket是否可用
当需要判断一个Socket是否可用的时候,不能简简单单判断是否为null,是否关闭,下面给出一个比较全面的判断Socket是否可用的表达式,这是根据Socket自身的一些状态进行判断的,它的状态有:
- bound:是否绑定
- closed:是否关闭
- connected:是否连接
- shutIn:是否关闭输入流
- shutOut:是否关闭输出流
socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()&& !socket.isInputShutdown() && !socket.isOutputShutdown()
建议如此使用,但这只是第一步,保证Socket自身的状态是可用的,但是当连接正常创建后,上面的属性如果不调用本方相应的方法是不会改变的,也就是说如果网络断开、服务器主动断开,Java底层是不会检测到连接断开并改变Socket的状态,所以,真实的检测连接状态还是得通过额外的手段
3.3.1 自定义心跳包
双方需要约定,什么样的消息属于心跳包,什么样的消息属于正常消息,我们定义前两个字节为消息的长度,那么我们就可以定义第3个字节为消息的属性,可以指定一位为消息的类型,1为心跳,0为正常消息。那么要做的有如下:
- 客户端发送心跳包
- 服务端获取消息判断是否是心跳包,若是丢弃
- 当客户端发送心跳包失败时,就可以断定连接不可用
3.4 设置端口重用SO_REUSEADDR
首先,创建Socket时,默认是禁止的,设置true有什么作用呢,Java API中是这么介绍的:
关闭 TCP 连接时,该连接可能在关闭后的一段时间内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态)。对于使用已知套接字地址或端口的应用程序而言,如果存在处于超时状态的连接(包括地址和端口),可能不能将套接字绑定到所需的 SocketAddress 上。
使用 bind(SocketAddress) 绑定套接字前启用 SO_REUSEADDR 允许在上一个连接处于超时状态时绑定套接字。
一般是用在绑定端口的时候使用,但是经过我的测试建议如下:
- 服务端绑定端口后,关闭服务端,重新启动后不会提示端口占用
- 客户端绑定端口后,关闭,即便设置ReuseAddress为true,即便能绑定端口,连接的时候还是会报端口占用异常
综上所述,不建议绑定端口,也没必要设置ReuseAddress,当然ReuseAddress的底层还是和硬件有关系的,或许在你的机器上测试结果和我不一样,若是如此和平台相关性差异这么大配置更是不建议使用了。
3.5设置关闭等待SO_LINGER
SO_LINGER选项用来设置延迟关闭的时间,等待套接字发送缓冲区中的数据发送完成。没有设置该选项时,在调用close()后,在发送完FIN后会立即进行一些清理工作并返回。如果设置了SO_LINGER选项,并且等待时间为正值,则在清理之前会等待一段时间。
以调用close()主动关闭为例,在发送完FIN包后,会进入FIN_WAIT_1状态。如果没有延迟关闭(即设置SO_LINGER选项),在调用tcp_send_fin()发送FIN后会立即调用sock_orphan()将sock结构从进程上下文中分离。分离后,用户层进程不会再接收到套接字的读写事件,也不知道套接字发送缓冲区中的数据是否被对端接收。如果设置了SO_LINGER选项,并且等待时间为大于0的值,会等待套接字的状态从FIN_WAIT_1迁移到FIN_WAIT_2状态。我们知道套接字进入FIN_WAIT_2状态是在发送的FIN包被确认后,而FIN包肯定是在发送缓冲区中的最后一个字节,所以FIN包的确认就表明发送缓冲区中的数据已经全部被接收。当然,如果等待超过SO_LINGER选项设置的时间后,还是没有收到FIN的确认,则继续进行正常的清理工作,Linux下也没有返回错误。从这里看来,SO_LINGER选项的作用是等待发送缓冲区中的数据发送完成,但是并不保证发送缓冲区中的数据一定被对端接收(对端宕机或线路问题),只是说会等待一段时间让这个过程完成。如果在等待的这段时间里接收到了带数据的包,还是会给对端发送RST包,并且会reset掉套接字,因为此时已经关闭了接收通道。
在使用这个选项来延迟关闭连接的时候有两个地方需要注意:
1. 进程会睡眠,直到状态不为FIN_WAIT_1、CLOSING、LAST_ACK(也就是接收到对FIN的ACK包),或者等待超时
2. 在等待的过程中如果接收到带数据的包还是会发送RST包
3.消耗更多的额外资源
TCP协议是一个通用的传输层协议,不关心上层具体的业务,如果要延迟关闭连接,最好是结合自己的业务和场景自己来管理,不要依赖这个选项。nginx的延迟关闭就是自己来管理的,觉得要比直接使用SO_LINGER选项好一些,并且不会导致进程阻塞。 ngxin在发送错误信息后,会等待一段时间,让用户把所有的数据都发送完。超过等待时间后,会直接关闭连接。通过lingering_close,nginx可以保持更好的客户端兼容性,避免客户端被reset掉。
SO_LINGER还有一个作用就是用来减少TIME_WAIT套接字的数量。在设置SO_LINGER选项时,指定等待时间为0,此时调用主动关闭时不会发送FIN来结束连接,而是直接将连接设置为CLOSE状态,清除套接字中的发送和接收缓冲区,直接对对端发送RST包。
3.6设置发送延迟策略TCP_NODELAY
一般来说当客户端想服务器发送数据的时候,会根据当前数据量来决定是否发送,如果数据量过小,那么系统将会根据Nagle 算法(暂时还没研究),来决定发送包的合并,也就是说发送会有延迟,这在有时候是致命的,比如说对实时性要求很高的消息发送,在线对战游戏等,即便数据量很小也要求立即发送,如果稍有延迟就会感觉到卡顿,默认情况下Nagle 算法是关闭的,
但是对延迟要求不是特别高下还是可以使用的,还是可以提升网络传输效率的。
3.7设置输出输出缓冲区大小SO_RCVBUF/SO_SNDBUF
- SO_SNDBUF:发送缓冲
- SO_RCVBUF:接收缓冲
默认都是8K,如果有需要可以修改,通过相应的set方法。不建议修改的太小,设置太小数据传输将过于频繁。太大了将会造成消息停留。
不过我对这个经过测试后有以下结论:
- 当数据填满缓冲区时,一定会发送
- 当数据没有填满缓冲区时也会发送,这个算法还是上面说的Nagle 算法
3.8设置保持连接存活SO_KEEPALIVE
虽然说当设置连接连接的读超时为0,即无限等待时,Socket不会被主动关闭,但是总会有莫名其妙的软件来检测你的连接是否有数据发送,长时间没有数据传输的连接会被它们关闭掉。
因此通过设置这个选项为true,可以有如下效果:当2个小时(具体的实现而不同)内在任意方向上都没有跨越套接字交换数据,则 TCP 会自动发送一个保持存活的消息到对面。将会有以下三种响应:
- 返回期望的ACK。那么不通知应用程序(因为一切正常),2 小时的不活动时间过后,TCP 将发送另一个探头。
- 对面返回RST,表明对面挂了,但是又好了,Socket依然要关闭
- 没有响应,说明对面挂了,这时候关闭Socket
所以对于构建长时间连接的Socket还是配置上SO_KEEPALIVE比较好
3.9异常:java.net.SocketException: Connection reset by peer
这个异常的含义是,我正在写数据的时候,你把连接给关闭了。这个异常在一般正常的编码是不会出现这个异常的,因为用户通常会判断是否读到流的末尾了,读到末尾才会进行关闭操作,如果出现这个异常,那就检查一下判断是否读到流的末尾逻辑是否正确