JavaSocket编程基础

1、BIO的基本模型图

 

java判断用户输入的是否是数字的 java判断是否有输入_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 会自动发送一个保持存活的消息到对面。将会有以下三种响应:

  1. 返回期望的ACK。那么不通知应用程序(因为一切正常),2 小时的不活动时间过后,TCP 将发送另一个探头。
  2. 对面返回RST,表明对面挂了,但是又好了,Socket依然要关闭
  3. 没有响应,说明对面挂了,这时候关闭Socket

  所以对于构建长时间连接的Socket还是配置上SO_KEEPALIVE比较好

3.9异常:java.net.SocketException: Connection reset by peer

这个异常的含义是,我正在写数据的时候,你把连接给关闭了。这个异常在一般正常的编码是不会出现这个异常的,因为用户通常会判断是否读到流的末尾了,读到末尾才会进行关闭操作,如果出现这个异常,那就检查一下判断是否读到流的末尾逻辑是否正确