文档

java体系技术文档


java Socket

<h1>java socket编程基础</h1> <h2>1. socket通信示例伪代码</h2> <h3>1.1 客户端伪代码</h3> <pre><code>socketClient(String ip, int port) { try{ //与服务端建立连接 Socket socket = new Socket(ip,port); //获取输出流 OutputStream os = socket.getOutputStream(); String message = "客户端发送消息"; os.write(message.getBytes("UTF-8")); //通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据 socket.shutdownOutput(); //获取输入流 InputStream in = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while((len = in.read(bytes)) != -1) { sb.append(new String(bytes,0,len,""UTF-8)); } }catch(Exception e){ throw e; }finally{ in.close(); os.close(); socket.close(); } }</code></pre> <h3>1.2 服务端伪代码</h3> <pre><code class="language-angular2html">socketServer(int port) { SocketServer socketServer = new SocketServer(port); while (true) { //server将一直等待连接的到来 Socket socket = socketServer.accept(); Runnable runnable = ()-&gt;{ try{ InputStream in = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while((len = in.read(bytes)) != -1) { sb.append(new String(bytes,0,len,"UTF-8")); } String message = "服务端响应消息"; OutputStream os = socket.getOutputStream(); os.write(message.getBytes("UTF-8")); }catch(Exception e){ throw e; }finally{ in.close(); os.close(); socket.close(); } } //提交线程池 threadPool.submit(runnable); } } </code></pre> <h2>2. 如何告知对方已经发送完成</h2> <h3>2.1 通过socket关闭</h3> <ul> <li>socket.close()</li> <li>当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了.</li> <li>客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息</li> <li>如果客户端想再次发送消息,需要重现创建Socket连接 <h3>2.2 通过Socket关闭输出流的方式</h3></li> <li>是调用方法socket.shutdownOutput(),而不是调用outputStream.close()</li> <li>如果关闭了输出流,那么相应的socket也将关闭,和关闭socket性质一样</li> <li>调用了shutdownOutput()之后,底层会告诉服务端这边已经发送完成,服务端可以进行后续操作了(返回消息给客户端或直接关闭socket)</li> <li>这种关闭方式不能再次发送消息给服务端,需要重新建立socket连接,但是可以接收服务端返回的消息 <h3>2.3 通过约定符号关闭</h3></li> <li>双方约定一个字符或字符串作为消息发送完成的标志</li> <li>优点:不需要关闭流,发送完一条消息后可以再次发送新的消息</li> <li>缺点:需要约定额外的标识符和特殊处理,并且标识符太简单容易出现在消息中,太复杂不好处理还占带宽 4.通过指定长度</li> <li>先指定后续命令的长度,然后读取指定长度的内容做为客户端发送的消息</li> <li>当然现在流行的就是,长度+类型+数据模式的传输方式</li> </ul> <h2>3. socket的其它知识</h2> <h3>3.1 客户端是否绑定端口</h3> <ul> <li>一般不建议绑定端口,socket会自动选取端口进行访问,绑定端口还有可能会造成程序的端口占用异常 <h3>3.2 读超时SO_TIMEOUT</h3></li> <li>TCP为长连接,只要不关闭,连接会一直持续下去,当一端因为异常导致连接没有关闭,另一方是不应该持续等下去的,所以应该设置一个读取的超时时间</li> <li>当超过指定的时间后,还没有读到数据,就假定这个连接无用,然后抛异常,捕获异常后关闭连接就可以了</li> <li>调用方法为setSoTimeOut(int timeout),timeout指定以毫秒为单位的超时值,设置为0表示持续等待下去 <h3>3.3 设置连接超时</h3></li> <li>这个连接超时和上面说的读超时不一样,读超时是在建立连接以后,读数据时使用的,而连接超时是在进行连接的时候,等待的时间</li> <li>调用方法setTimeOut(int timeout) <h3>3.4 判断socket是否可用</h3> <p>当需要判断一个Socket是否可用的时候,不能简简单单判断是否为null,是否关闭。应当根据socket的一些自身状态 进行判断,它的状态有:bound(是否绑定)、closed(是否关闭)、connected(是否连接)、shutIn(是否关闭输入流)、 shutOut(是否关闭输出流)。具体判断条件为: socket != null &amp;&amp; socket.isBound() &amp;&amp; !socket.isClosed() &amp;&amp; socket.connected() &amp;&amp; !socket.isInputShutdown() &amp;&amp; !socket.isOutputShutdown() 如果网络断开、服务器主动断开,Java底层是不会检测到连接断开并改变Socket的状态,所以,真实的检测连接状态还是得通过额外的手段,有两种方式:</p> <h4>3.4.1 自定义心跳包</h4> <p>双方需要约定,什么样的消息属于心跳包,什么样的消息属于正常消息,可以指定一位为消息的类型,1为心跳,0为正常消息,那么要做的如下:</p></li> <li>客户端发送心跳包</li> <li>服务端获取消息判断是否是心跳包,若是,则丢弃</li> <li>当客户端 <h4>3.4.2 通过发送紧急数据</h4></li> <li>Socket自带一种模式,那就是发送紧急数据,这有一个前提,那就是服务端的OOBINLINE不能设置为true(设为true,服务端会捕获紧急数据,造成数据混乱,需要额外处理),它的默认值是false</li> <li>发送紧急数据调用的方法:socket.sendUrgentData()</li> <li>我们只要发送紧急数据即可,因为OOBINLINE为false的时候,服务端会丢弃掉紧急数据。当发送紧急数据报错后,我们就知道连接断开了 <h4>3.4.3 真的需要判断连接断开吗?</h4> <ol> <li>发送心跳成功时确认连接可用,当再次发送消息时能保证连接还可用吗?即便中间的间隔很短?</li> <li>如果连接不可用了,你会怎么做?重新建立连接再次发送数据?还是说单单只是记录日志?</li> <li>如果你打算重新建立连接,那么发送心跳包的意义何在?为何不在发送异常时再新建连接?</li> </ol></li> </ul> <p>考虑了上面的问题,发现判断连接是否断开根本没有必要,判断连接是否可用是通过捕获异常来判断的。那么我们完全可以在发送消息报出IO异常的时候,在异常中重新发送一次即可。</p> <h4>3.4.5 设置端口重用SO_REUSEADDR</h4> <p>关闭 TCP 连接时,该连接可能在关闭后的一段时间内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态)。对于使用已知套接字地址或端口的应用程序而言,如果存在处于超时状态的连接(包括地址和端口),可能不能将套接字绑定到所需的 SocketAddress 上。</p> <ul> <li>使用socket.bind(SocketAddress address)启用端口重用</li> <li>启用端口重用之后,允许在上一个连接处于超时状态时绑定套接字</li> <li>具体表现为当客户端或服务端绑定端口后,启用端口重用之后,重启应用程序不会报端口占用异常 <h4>3.4.6 设置关闭等待SO_LINGER</h4> <p>该设置仅影响套接字的关闭,当调用Socket的close方法后,没有发送的数据将不再发送,设置这个值的话,Socket会等待指定的时间发送完数据包。</p> <h4>3.4.7 设置发送延迟策略TCP_NODELAY</h4> <p>一般来说当客户端想服务器发送数据的时候,会根据当前数据量来决定是否发送,如果数据量过小,那么系统将会根据 Nagle 算法来决定发送包的合并,也就是说发送会有延迟。 对于实时性要求高的应用来说是很致命的。所以可以设置该属性来立刻发送。</p> <h4>3.4.8 设置发送/接收缓冲区大小 SO_RCVBUF/SO_SNDBUF</h4> <p>默认都是8K,如果有需要可以修改,通过相应的set方法。socket.setSendBufferSize(int size);socket.setReceiveBufferSize(int size);</p> <h4>3.4.9 异常:java.net.SocketException: Connection reset by peer</h4> <p>这个异常的含义是,我正在写数据的时候,你把连接给关闭了。这个异常在正常的编码下是不会出现的, 因为用户通常会判断是否读到流的末尾了,读到末尾才会进行关闭操作,如果出现这个异常,那就检查一下判断是否读到流的末尾逻辑是否正确。</p> <h2>4. 关于socket的理解</h2> <p>我们都知道TCP/IP的五层模型,应用层、传输层、网络层、数据链路层、物理层。应用层有Telnet、FTP等应用,socket实际上是归属应用层的。</p></li> </ul> <p><code>Socket socket = serverSocket.accept();</code></p> <p>在什么情况获取到这个Socket呢,通过理论加测试,结论是在三次握手操作后,系统才会将这个连接交给应用层,ServerSocket 才知道有一个连接过来了。 那么系统当接收到一个TCP连接请求后(传输层),如果上层(应用层serverSocket)还没有接受它,那么系统将缓存这个连接请求,缓存肯定是有限度的,当超过指定的缓存数量后,系统将会拒绝连接。</p> <p>假如缓存的TCP连接请求发送来数据,那么系统也会缓存这些数据,等待SocketServer获得这个连接的时候一并交给它。 换句话说,系统接收TCP连接请求放入缓存队列,而SocketServer从缓存队列获取Socket。</p> <p>为了让服务端知道发送完消息的,关闭输出流的操作:socket.shutdownOutput();其实是对应四次挥手的第一次。</p> <h3>4.1 拆包和黏包</h3> <h4>4.1.1 拆包</h4> <p>当一次发送(Socket)的数据量过大,而底层(TCP/IP)不支持一次发送那么大的数据量,则会发生拆包现象。</p> <h4>4.1.2 黏包</h4> <p>当在短时间内发送(Socket)很多数据量小的包时,底层(TCP/IP)会根据一定的算法(指Nagle)把一些包合作为一个包发送。</p> <p>黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还 是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms), 那么还是不建议关闭这种优化。</p> <p>如果不希望发生黏包,那么通过禁用TCP_NODELAY即可。调用方法socket.setTcpNoDelay(boolean on);</p>

页面列表

ITEM_HTML