文章目录
TCP与UDP
- TCP:传输控制协议。TCP是面向连接的通信协议,TCP提供两台计算机之间的可靠无差错的数据传输
- UDP:用户数据报协议。UDP是无连接通信,不保证可靠数据的传输,但能够向若干个目标传输数据,接收来自若干个源的数据。
- 利用UDP通信的两个程序是平等的,没有主次之分,两个程序的代码可以是一样的。利用TCP协议进行通信的两个应用程序是有主次之分的,一个称为服务器程序一个称为客户端程序,两者的功能的编写大不一样。
套接字
Socket
可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket
中,该Socket
将这段信息发送给另一个Socket
中,使这段信息能传送到其他程序中。java中Socket
代表了客户端流套接字。ServerSocket
代表了服务程序流套接字Socket
与ServerSocket
的交互过程
客户端Socket
-
可通过
Socket
的getInputStream()
方法从服务程序获得输入流读传送来的信息,也可通过Socket
的getOutputStream()
方法获得输出流来发送信息。 -
构造
Socket
,Socket(host, port)
在创建后自动连接,如果连接成功就返回Socket
对象,如果连接失败就抛出IOException
,Socket()
只是创建,需要手动连接。@Test public void testCreateAndConnect() { Socket socket = null; try { int port = 80; String host = "httpbin.org"; //创建并自动连接到服务端 socket = new Socket(host, 80); System.out.println("connect server success ,in port " + 80); //关闭连接 socket.close(); //创建 socket = new Socket(); SocketAddress socketAddress = new InetSocketAddress(host, port); //手动连接,并设置超时时间,如果在2s秒的时间内连接成功就返回, //如果在2s内出现其他异常则抛该异常,如果既没有抛出其他异常也没有连接成功则抛出SocketTimeoutException异常 socket.connect(socketAddress, 2000); System.out.println("connect server success ,in port " + port); } catch (UnknownHostException e) { e.printStackTrace(); } catch (ConnectException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
关闭Socket
-
当客户端与服务器的通信结束,应该及时关闭
Socket
,以释放Socket
占用的包括端口在内的各种资源。Close()
负责关闭Socket
,当一个Socket
对象被关闭,它不能再通过它的输入流和输出流进行IO操作。否则会导致IOException
-
检测方法
isClosed():如果Socket已经连接到远程主机,并且还没有关闭,返回true,否则false isConnected:如果Socket曾经连接到远程主机,则返回true,否则返回false isBound():如果Socket已经与一个本地端口邦定,则返回true,否则返回false
-
如果判断一个Socket对象当前是否处于连接状态
Boolean isConnected = socket.isConnected()&&!socket.isClosed();
异常
-
客户端连接服务器可能抛出的异常
UnknowHostException
:如果无法识别主机的名字或IP,就会抛出这种异常ConnectException
:如果没有服务器进程监听指定的端口(指定ip地址的机器不能找到或者是该ip存在,但找不到指定的端口进行监听),或者服务器进程拒绝连接(Connection refused:connect),就会抛此异常SocketTimeoutException
:如果等待数据超时,就会抛出异常BindException
:如果无法把Socket对象与指定的本地IP地址或端口号绑定,就会抛异常NoRouteToHostException
:没有路由连接到主机,无法到达远程主机,
-
ConnectException
案例@Test public void testConnectException() throws Exception { Socket socket = new Socket(); socket.connect(new InetSocketAddress("127.0.0.1", 8888), 10000); System.out.println("socket连接成功...."); Socket socket2 = new Socket(); socket2.connect(new InetSocketAddress("127.0.0.1", 8888), 1); System.out.println("socket2连接成功...."); }
-
SocketTimeoutException
案例//服务端 public class SimpleServer { public static void main(String[] args) throws IOException, InterruptedException { ServerSocket serverSocket = new ServerSocket(8888, 1); LockSupport.park(); } } @Test public void testSocketTimeoutException() throws Exception { Socket socket = new Socket(); socket.connect(new InetSocketAddress("127.0.0.1", 8888), 1); System.out.println("socket连接成功...."); } @Test public void testReadTimeOut() { Socket socket = new Socket(); long startTime = 0; try { socket.connect(new InetSocketAddress("127.0.0.1", 8888), 10000); System.out.println("socket连接成功...."); //此处仅仅是设置了一个值,只有调用read()方法,超时时间才真正生效 socket.setSoTimeout(2000); startTime = System.currentTimeMillis(); int read = socket.getInputStream().read(); } catch (IOException e) { e.printStackTrace(); } finally { long endTime = System.currentTimeMillis(); System.out.println("执行时间:" + (endTime - startTime)); } }
-
BindException
- 无法把
Socket
对象与指定的本地Ip地址或端口邦定,就会抛出此异常。 - 使用
netstat –an
查看Listening状态的端口,找到被占用的端口
- 无法把
-
SocketException
- Socket is closed
- 该异常在客户端和服务器端均可能发生。异常的原因是己方主动关闭了连接后(调用了Socket的close方法)再对网络连接进行读写操作
- Connection reset
- Connection reset或者Socket write error:该异常在客户端和服务器端均有可能发生,引起该异常的原因有两个,第一个就是如果一端的Socket被关闭(或主动关闭或者因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer)。另一个是一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常(Connection reset)。简单的说就是由连接断开后的读和写操作引起的。
- Connection reset by peer:服务器的并发连接数已经超过了承载量,服务器会将一些连接关闭
- Broken pipe
- Broken pipe:该异常在客户端和服务器均有可能发生。在
SocketException
异常的第一种情况中(也就是抛出 SocketExcepton:Connect reset by peer:Socket write error后),如果再继续写数据则抛出该异常。前两个异常的解决方法是首先确保程序退出前关闭所有的网络连接,其次是要检测对方的关闭连接操作,发现对方关闭连接后自己也要关闭该连接。
- Broken pipe:该异常在客户端和服务器均有可能发生。在
- Too many open files:操作系统的中打开文件的最大句柄数受限所致,常常发生在很多个并发用户访问服务器的时候。因为为了执行每个用户的应用服务器都要加载很多文件(new一个socket就需要一个文件句柄),这就会导致打开文件的句柄的不足。
- 尽量把类打成jar包,因为一个jar包只消耗一个文件句柄,如果不打包,一个类就消耗一个文件句柄。
- java的GC不能关闭网络连接打开的文件句柄,如果没有执行close()则文件句柄将一直存在,而不能被关闭
- 设置socket的最大打开数来控制这个问题。对操作系统做相关的设置,增加最大文件句柄数量
- Socket is closed
-
对于一个Http请求来说,一次http请求,必定会有三个阶段,一:建立连接;二:数据传送;三,断开连接。当建立连接在规定的时间内(
ConnectionTimeOut
)没有完成,那么此次连接就结束了。后续的SocketTimeOutException
就一定不会发生。只有当连接建立起来后,也就是没有发生ConnectionTimeOutException
,才会开始传输数据,如果数据在规定的时间内(SocketTimeOut
)传输完毕,则断开连接。否则,触发SocketTimeOutException
。关于java的ConnectionTimeOut
和SocketTimeOutException
的详细分析,可以参见Socket笔记之深入分析java中的ConnectionTimedOut和Socket笔记之Read timed out深入分析
Socket选项
-
Socket
有以下几个选项TCP_NODELAY
:表示立即发送数据SO_RESUSEADDR
:表示是否允许重用Socket所绑定的本地地址SO_TIMEOUT
:表示接收数据时的等待超时时间SO_LINGER
:表示当执行Socket的close方法时,是否立即关闭底层的SocketSO_SNFBUF
:表示发送数据的缓冲区的大小SO_RCVBUF
:表示接收数据的缓冲区大小SO_KEEPALIVE
:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭OOBINLINE
:表示是否支持发送一个字节的TCP紧急数据
-
SO
就是SocketOptions
的缩写public interface SocketOptions { public final static int TCP_NODELAY = 0x0001; public final static int SO_BINDADDR = 0x000F; public final static int SO_REUSEADDR = 0x04; public final static int SO_BROADCAST = 0x0020; public final static int IP_MULTICAST_IF = 0x10; public final static int IP_MULTICAST_IF2 = 0x1f; public final static int IP_MULTICAST_LOOP = 0x12; public final static int IP_TOS = 0x3; public final static int SO_LINGER = 0x0080; public final static int SO_TIMEOUT = 0x1006; public final static int SO_SNDBUF = 0x1001; public final static int SO_RCVBUF = 0x1002; public final static int SO_KEEPALIVE = 0x0008; public final static int SO_OOBINLINE = 0x1003; }
缓冲区
-
缓冲区(SO_RCVBUF, SO_SNDBUF)越大,操作效率越高。在以太网上,4KB的缓冲区大小是不够的,将4KB提高到16KB会是吞吐量提高40%(《Fundamental Networking in java》)
-
Socket
缓冲区大小应始终至少为连接的Maximum segment size(MSS)
的三倍
-
发送缓冲区大小应至少与另一端的接收缓冲区一样大
-
对于一次发送大量数据的应用程序,将发送缓冲区的大小增加到48k或者64Kb可能是对应用程序进行的最有效的性能改进
-
无论缓冲区大小是多少,都应该用通过
BufferedOutputStream
或ByteBuffer
写入缓冲区大小的块来改善TCP的性能
SO_KEEPALIVE
- 表示底层TCP协议的心跳机制。true为连接保持心跳,默认值为false。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。 一般情况下,我们不需要开启,比如Netty默认关闭该功能。
TCP_NODELAY
- 在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1个字节,而数据包的头却有几十个字节(IP头+TCP头)时,系统会在发送之前先将较小的包合并到较大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;在默认情况下,Nagle算法是开启的。
- Nagle算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet等),使用这种方式传输数据会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle算法时就使用它,不需要时就关闭它。而使用
setTcpToDelay
正好可以满足这个需求。当使用setTcpNoDelay(true)
将Nagle算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。 - 如果
Socket
底层实现不支持TCP_NODELAY
选项,那么setTcpNoDelay
和getTcpNoDelay
就会抛SocketException
SO_LINGER
SO_LINGER
选项指定了Socket
关闭时如何处理尚未发送的数据报。默认情况下,close()方法将立即返回,但系统仍会发送剩余的数据。如果延迟时间设置为0,那么当Socket关闭时,所有未发送的数据包将被丢弃。如果SO_LINGER
打开而且延迟时间设置为任意正数,close()方法将会阻塞(阻塞时间为指定的秒数),等待发送数据和接收确认。当过去相应秒数后,Socket
关闭,所有剩余的数据都不会发送,也不会收到确认。
服务类型
-
用户可以根据自己的需求选择不同的服务类型。例如发送视频需要较高的带宽,快速达到目的地,以保证接收方看到连续的画面。而发送电子邮件可以使用较低的带宽
-
IP规定了4中服务类型,用来定性地描述服务的质量
- 低成本:发送成本低
- 高可靠性:保证把数据可靠地送达目的地
- 高吞吐量:一次可以接收或发送大批量的数据
- 最小延迟:传输数据的速度快,把数据快速送达目的地
-
四种服务类型可以组合,例如,同时要求获得最高可靠性和最小延迟。
public void setTrafficClass(int tc) public int getTrafficClass() throws SocketException 低成本:IPTOS_LOWCOST (0x02) 高可靠性:IPTOS_RELIABILITY (0x04) 最高吞吐量:IPTOS_THROUGHPUT (0x08) 最小延迟:IPTOS_LOWDELAY (0x10) socket = new Socket(host,port); socket.setTrafficClass(0x04|0x10);//0x04|0x10 进行位或运算
设定连接时间、延迟和带宽的相对重要性
-
jdk1.5提供了
setPerformancePreferences
,用于设定三个指标的重要性,三个整数之间的相对大小决定了相应参数的相对重要性。connectionTime:表示用最少时间建立连接 latency:表示最小延迟 bandwidth:表示最高带宽 public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)