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 = ()->{
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 && socket.isBound() && !socket.isClosed() && socket.connected() && !socket.isInputShutdown() && !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>