分享

java socket/Serversocket编程详解(中/英文)

 點點滴滴 2012-08-03

socket /套接字 

    Sockets let you send raw streams of bytes back and forth between two computers, giving you fairly low-level access to the TCP/IP protocol. See the File I/O Amanuensis for sample code to do that. In TCP/IP each computer has a name, such as roedy.mindprod.com. However, various TCP/IP programs could be running on that computer. Each Socket gets a assigned a number called a port. The HTTP server would usually be assigned 80. DbAnywhere is usually 8889. This way you can specify which service on the local or remote machine you want to connect with. The Socket is specified like this: roedy.mindprod.com:8889. 



     socket提供了在主机之间传递原始字节流的功能,以比较底层的方式访问tcp/ip协议层.可以类似访问文件i/o的方式实现这一功能(在unix 中,系统资源是以文件的方式进行访问的,其中也包括网络资源).tcp/ip协议规定,每台主机都有一个名称,例如 roedy.mindprod.com.然而,同一台主机上有可能同时运行很多tcp/ip程序.每个socket被指派了一个叫做端口的数字以加以区分 不同的应用或者连接.http应用服务器的端口一般被指定为80,DbAnywhere通常指定为8889.我们通过这种方式区分你向远程或者本地主机请 求连接的服务.一个socket被定义为 地址:端口, 例如 roedy.mindprod.com:8889 


    Flush / 刷新 


    If you write to a Socket, you usually need to call flush to force the data out onto the net. If you fail to do that, you could wait forever for a response because your complete query was never sent. You don’t need flush if you are sending a steady stream of data that will push earlier data out onto the net. 



     如果向一个socket写入数据,通常需要调用flush方法去把数据发送到网络.如果操作失败,可能由于完整的请求信息未曾发送成功而导致持续等待响应.如果使用稳定的数据流的方式,不需要调用flush方法,因为数据流会自动把先前的数据发送到网络. 



    Blocking Read / 读堵塞 

   If you read from a Socket, you can hang waiting forever if you use a blocking read. Socket.setSoTimeout controls the timeout. The read will eventually die when the Socket connection fails. This will happen when: 

        * You close the Socket at this end. 
        * The far end sends a disconnect signal. 
        * TCP cannot get an acknowlegement for packets it has sent, even after several retransmissions. These packets could either be data sent by the application, or keep-alive messages (if keep-alive has been turned on). Don’t confuse this with the meaningless HTTP Keep-Alive parameter. 



      由socket读取数据时,如果使用堵塞的读操作,可能会导致永久地等待.Socket的setSoTimeout方法 控制了超时的期限.在socket连接失败的情况下,读取数据的操作最终会被停止. 

这种情况通常发生在以下几种情况: 

        1.本地关闭了socket, 

        2.远程主机/终端发送了断开连接的信号, 

        3.tcp协议实现在尝试多次重发数据仍无法获得对方针对已发送数据包的确认信息,或者无法获得keep-alive的信息(如果tcp协议的keep- alive选项已经被启用).另外不要和http协议的keep-alive参数相混淆.(http的keep-alive选项是指客户端与服务器之间建 立有效的长连接,避免了重复建立连接的消耗,尤其对提供静态资源访问的网站能够很大的提高访问效率) 



    Timeouts /超时 


    Java offers Socket.setSoTimeout to control how long you are willing to wait for a read to complete and Socket.setSoLinger to control how long it lingers, (waits to close when there are still unsent data). When you shutdown, the other end should continue to read any buffered data to let the other end close before closing itself. setSoTimeout has no effect on how long you are willing to wait for a write (how long you are willing to wait for the other end to accept data), just on how long you are willing to wait for the other end to produce data. 

    To add to the misery, Windows partially ignores the timeout. On connect, the JVM tries to resolve the hostname to IP/port. Windows tries a netbios ns query on UDP port 137 with a timeout of 1.5 seconds, ignores any ICMP port unreachable packets and repeats this two more times, adding up to a value of 4.5 seconds. I suggest putting critical hostnames in your HOSTS file to make sure they are resolved quickly. Another possibility is turning off NETBIOS altogether and running pure TCP/IP on your LAN. 



    socket的java实现接口提供了setSoTimeout方法设置希望等待完成读取操作的时间期限,提供setSoLinger方法控制关闭等待期 限(等待尚未发送的数据,然后关闭连接). 当一方关闭连接时,另一方仍会在读取到缓冲区中的通知关闭连接的数据以后关闭连接(这句话不知道这样翻译是否准确,不过实际操作应该是这样的,可以这样理 解,当一端单方面关闭连接的时候,应该通知另一方你已经关闭连接,以便对方获悉并且关闭连接).setSoTimeout选项对等待完成写操作的期限没有 影响(等待对方对方接收数据的期限),只和等待对方产生数据的期限有关.(setSoTimeout和对方发送响应数据是否超时有关和对方何时接收数据没 有关系). 



     比较令人苦闷的是,windows系统不负责任地忽略超时.对于一个连接.java虚拟机努力将域名解析为ip地址和端口号.而windows使用udp 的137端口向域名解析服务器发送域名解析查询,超时设为1.5秒.忽略了任何的icmp端口不可访问的数据包并且连续再重复发送两次相同的请求(一共是 三次).总计需要等待4.5秒.因此强烈建议把常用的域名地址和对应的ip地址和端口写在hosts文件中以确保可以迅速解析.另外就是在局域网完全中关 闭windows的NETBIOS服务,完全使用tcp/ip访问资源. 



    Disconnet Detection / 探测连接关闭 


    Since TCP/IP sends no packets except when there is traffic, without Socket.setKeepAlive( true ), it has no way of noticing a disconnect until you start trying to send (or to a certain extent receive) traffic again. Java has the Socket.setKeepAlive( true ) method to ask TCP/IP to handle heartbeat probing without any data packets or application programming. Unfortunately, you can’t tell it how frequently to send the heartbeat probes. If the other end does not respond in time, you will get a socket exception on your pending read. Heartbeat packets in both directions let the other end know you are still there. A heartbeat packet is just an ordinary TCP/IP ack packet without any piggybacking data. 



    当网络繁忙的时候,tcp/ip无法发送数据包.如果没有设定socket的setKeepAlive为true,我们无法获悉一个连接已经关闭除非试图 再次进行发送操作(或者进行某些接收操作).java通过设定socket的setKeepAlive为true的方式要求tcp/ip协议进行心跳检 测,不需要发送任何数据包或者应用级别的编程.然而不幸地是你无法肯定tcp/ip以怎样的频率发送心跳探测信号.如果另一方无法及时响应,当你试图进行 读取操作的时候就会产生socket的异常.心跳包使双方都能获知对方是否保持连接.心跳包只是一个普通的tcp/ip的ack报文不需要搭载任何的其他 数据. 


    When the applications are idling, your applications could periodically send tiny heartbeat messages to each other. The receiver could just ignore them. However, they force the TCP/IP protocol to check if the other end is still alive. These are not part of the TCP/IP protocol. You would have to build them into your application protocols. They act as are-you-still-alive? messages. I have found Java’s connection continuity testing to be less that 100% reliable. My bullet-proof technique to detect disconnect is to have the server send an application-level heartbeat packet if it has not sent some packet in the last 30 seconds. It has to send some message every 30 seconds, not necessarily a dummy heartbeat packet. The heartbeat packets thus only appear when the server is idling. Otherwise normal traffic acts as the heartbeat. The Applet detects the lack of traffic on disconnect and automatically restarts the connection. The downside is your applications have to be aware of these heartbeats and they have to fit into whatever other protocol you are using, unlike relying on TCP/IP level heartbeats. 



     当应用处于空闲状态的时候,你的应用可以间断地向彼此发送小的心跳信息.接收者可以完全忽视它们,但是它们强制tcp/ip协议去核实另一方是否存活.这 不是tcp/ip协议通信规范的一部分,你需要建立自己的心跳协议,例如 发送内容为' are-you-still-alive? '的信息,原作者通过测试发现java的连接持续性并非100%的可靠.他的银弹技术是通过服务端每隔30秒发送一个应用级别的心跳包,如果最近30秒内 没有接收到任何数据包.服务器必须每隔30秒发送一个数据包,不一定必须是傀儡的心跳数据包.心跳数据包只当服务器空闲的时候才会产生.否则的话,普通的 网络通信就可以替代心跳数据包的功能.applet探测发现由于断开连接导致的通信中断后就会重新建立连接.负面影响是你的应用必须时时关注这些心跳状 态,并且如果你使用其它网络协议你也要实现相应的心跳协议,不同余依赖于tcp/ip层的心跳. 


    However, it is simpler to use the built-in Socket.setKeepAlive( true ) method to ask TCP/IP to handle the heartbeat probing without any data packets or application programming. Each end with nothing to say just periodically sends an empty data packet with its current sequence, acknowledgement and window numbers. 


   然而,使用socket内置的setKeepAlive(true)方法去要求tcp/ip进行心跳探测不使用任何数据包或者应用级别地编程实现看起来更加容易一些.每个终端只需间歇地发送一个包含当前序列的空的数据包,确认信息和滑动窗口号就可以了. 


    The advantage of application level heartbeats is they let you know the applications at both ends are alive, not just the communications software. 



    应用级别的心跳优点在于它们能够使你了解两端的应用都是否存活,而不在于只是通信软件. 



Server Side Socketing /服务器端套接字 


    For a server to accept connections from the outside world, first it opens a ServerSocket on a port, but not connected to any client in particular. 



    对于一个接收外部连接的服务器,首先在某个没有连接任何客户端的端口上开启一个serversocket,代码如下 


    ServerSocket serverSocket = new ServerSocket( port ); 

    Then it calls accept, which blocks until a call comes in. 

    Socket clientSocket = serverSocket.accept(); 

    At that point a new ordinary Socket gets created that is connected to the incoming caller. Usually the server would spin off a Thread, or assign a Thread from a pool to deal with the new Socket and loop back to do another accept. 



     当接收到一个请求时会新建一个的普通的socket,通常服务器会启动一个线程或者由线程池中取出一个线程处理新产生的socket,然后循环处理下一个请求. 


    You can set up your miniature server even if you don’t have a domain name. They can get to you by name: ip:port e.g. 65.110.21.43:2222. Even if your are behind a firewall, you use the external facing IP of the firewall. You must then configure your firewall to let incoming calls through and to direct them to the correct server on the lan. 



     即使你并不拥有一个域名,你也可以建立自己的服务器.他人可以通过ip地址和端口的方式( e.g. 65.110.21.43:2222)访问你的服务器(如果在广域网上这要求你拥有自己的固定ip,这一般比拥有域名的成本还要高,不过在局域网内你可以 尝试局域网地址),如果你在处于防火墙保护的局域网内,你可以使用防火墙的对外ip.你必须配置你的防火墙以便请求数据包可以通过并且访问局域网内正确的 服务器. 



    Flow Control / 流控制 


    With Socket.setReceiveBufferSize() you can hint to the underlying OS how much to buffer up incoming data. It is not obligated to listen to you. Don’t confuse this with the buffer on the BufferedInputStream. This is the lower level buffer on the raw socket. Large buffers are not always desirable. Using small buffers can tell the other end you are getting behind, and it won’t send data as quickly. If the data is real time, and the amount of data sent is variable depending on how fast you process it, large buffers mean you can get way behind and never catch up. 



     使用socket的setReceiveBufferSize()方法你可以告诉底层的操作系统缓存多大的接收数据.但是这并非完全由你决定.不要将 socket的缓冲区和BufferedInputStream的缓冲区混淆.这是原始socket的底层的缓冲区.过大的缓冲区并不总能很好地满足需 要.使用小的缓冲区能够通知另一端你的处理速度已经落后了,因此对方不会继续马上发送数据过来(大的缓冲区,对方发送过来的数据有可能还没有读取并被处 理,但还留有很大的空间,因此对方会继续发送数据填满余下的空间,但是有可能导致大量的数据堆积在缓冲区中无法处理,理想状态是使用小的缓存区,处理完当 前数据后在接收,处理下一个数据).如果数据不是实时的,发送过来的数据量动态地依赖于处理数据的速度,过大的缓冲区会导致你处理的数据量一直落后于接收 的数据量,并且永远无法赶上. 


    There is a mysterioous method Socket.setTcpNoDelay( true ) to "disable Nagle’s algorithm". As is typical, there is no explanation what Nagle’s algorinthm is. My TCP/IP text book makes no mention of it. If you are dealing with near real-time data then you may want to look into disabling Nagle’s algorithm. That algorithm attempts to ensure that TCP doesn’t send lots of undersized IP packets by buffering-up submitted data and keeping it for typically for a few milliseconds to see if you are going to give it some more data that could go into the same packet. I am not sure if flush is sufficient to send a packet on its way immediately. 



     socket的setTcpNoDelay( true )很神秘地用来关闭Nagle算法.正如这里不解释Nagle算法一样,这里也不讨论这个setTcpNoDelay方法. 如果你处理近乎实时的数据,你可能会研究如何关闭Nagle算法.Nagle算法通过暂存已经提交发送的数据包许多毫秒的时间以便判断是否还需要向这个数 据包写入更多数据,确保tcp不发送大量的长度过小的数据包.我不确定是否flush方法能够充分地立即发送一个数据包. 



   Graceful Shutdown / 优雅地关闭 


    If you simply close a socket, you can lose data that were previously sent but which have not yet been delivered. You may chop things off in mid message. So, how to shut down gracefully? My approach is this. When the client wants to shut down, it sends a close message. The server echos it back and on receipt of the close message, the client closes the socket. That way the client is guaranteed to be unhooked from waiting on a read, and you are guaranteed the server and client each recieved the last remaining messages before the socket was closed. 



     如果你简单地关闭一个socket连接,你可能会丢失先前发送但并未抵达(交付)的数据.这可能会导致数据不完整.所以,如果优雅地关闭连接呢?作者的理 论是:当客户端试图关闭连接时,它首先要发送一条关闭信息.服务器原样返回关闭信息内容和确认关闭信息(增加确认关闭信息的做法可能是为了避免发送超时的 数据包返回给发送者,两者内容可能是相同的),客户端收到确认信息后关闭连接.这时客户端要确保解除等待读取操作的状态,并且你要确保客户端和服务器在关 闭前都收到了最后的信息. 




 

本篇文章观点和例子来自 《Java网络编程精解》, 作者为孙卫琴, 出版社为电子工业出版社。

 

      在客户/服务器通信模式中, 客户端需要主动创建与服务器连接的 Socket(套接字), 服务器端收到了客户端的连接请求, 也会创建与客户连接的 Socket. Socket可看做是通信连接两端的收发器, 服务器与客户端都通过 Socket 来收发数据.

 

这篇文章首先介绍Socket类的各个构造方法, 以及成员方法的用法, 接着介绍 Socket的一些选项的作用, 这些选项可控制客户建立与服务器的连接, 以及接收和发送数据的行为.

 

一. 构造Socket

 

     Socket的构造方法有以下几种重载形式:  

  1. Socket()
  2. Socket(InetAddress address, int port) throws UnknowHostException, IOException
  3. Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
  4. Socket(String host, int port) throws UnknowHostException, IOException
  5. Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException

     除了第一个不带参数的构造方法以外, 其他构造方法都会试图建立与服务器的连接, 如果连接成功, 就返回 Socket对象; 如果因为某些原因连接失败, 就会抛出IOException .

 

1.1 使用无参数构造方法, 设定等待建立连接的超时时间

   

  Socket socket = new Socket();
  SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
  socket.connect(remoteAddr, 60000);  //等待建立连接的超时时间为1分钟

 

      以上代码用于连接到本地机器上的监听8000端口的服务器程序, 等待连接的最长时间为1分钟. 如果在1分钟内连接成功则connet()方法顺利返回; 如果在1分钟内出现某种异常, 则抛出该异常; 如果超过1分钟后, 即没有连接成功, 也没有出现其他异常, 那么会抛出 SocketTimeoutException. Socket 类的 connect(SocketAddress endpoint, int timeout) 方法负责连接服务器, 参数endpoint 指定服务器的地址, 参数timeout 设定超时数据, 以毫秒为单位. 如果参数timeout 设为0, 表示永远不会超时, 默认是不会超时的. 

 

1.2 设定服务器的地址

 

      除了第一个不带参数的构造方法, 其他构造方法都需要在参数中设定服务器的地址, 包括服务器的IP地址或主机名, 以及端口:

  Socket(InetAddress address, int port)              //第一个参数address 表示主机的IP地址
  Socket(String host, int port)                              //第一个参数host 表示主机的名字

 

      InetAddress 类表示服务器的IP地址, InetAddress 类提供了一系列静态工厂方法, 用于构造自身的实例, 例如:

 

  //返回本地主机的IP地址
  InetAddress addr1 = InetAddress.getLocalHost();
  //返回代表 "222.34.5.7"的 IP地址
  InetAddress addr2 = InetAddress.getByName("222.34.5.7");
  //返回域名为"
www."的 IP地址
  InetAddress addr3 = InetAddress.getByName("
www.");

 

 

1.3 设定客户端的地址

 

      在一个Socket 对象中, 即包含远程服务器的IP 地址和端口信息, 也包含本地客户端的IP 地址和端口信息. 默认情况下, 客户端的IP 地址来自于客户程序所在的主机, 客户端的端口则由操作系统随机分配. Socket类还有两个构造方法允许显式地设置客户端的IP 地址和端口:

 

  //参数localAddr 和 localPort 用来设置客户端的IP 地址和端口
  Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
  Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException

 

     如果一个主机同时属于两个以上的网络, 它就可能拥有两个以上的IP 地址. 例如, 一个主机在Internet 网络中的IP 地址为 "222.67.1.34", 在一个局域网中的IP 地址为 "112.5.4.3". 假定这个主机上的客户程序希望和同一个局域网的一个服务器程序(地址为:"112.5.4.45: 8000")通信, 客户端可按照如下方式构造Socket 对象:

     

  InetAddress remoteAddr1 = InetAddress.getByName("112.5.4.45");
  InetAddress localAddr1 = InetAddress.getByName("112.5.4.3");
  Socket socket1 = new Socket(remoteAddr1, 8000, localAddr1, 2345);   //客户端使用端口2345

 

 

 1.4 客户连接服务器时可能抛出的异常

 

       当Socket 的构造方法请求连接服务器时, 可能会抛出下面的异常.

  • UnKnownHostException: 如果无法识别主机的名字或IP 地址, 就会抛出这种异常.
  • ConnectException: 如果没有服务器进程监听指定的端口, 或者服务器进程拒绝连接, 就会抛出这种异常.
  • SocketTimeoutException: 如果等待连接超时, 就会抛出这种异常.
  • BindException: 如果无法把Socket 对象与指定的本地IP 地址或端口绑定, 就会抛出这种异常.

       以上4中异常都是IOException的直接或间接子类.      如图2-1所示.

 

        IOException-------  UnknownHostException

                              |---- InterruptedIOException  -----------  SocketTimeoutException

                              |---- SocketException              -----------  BindException

                                                                             |----------  ConnectException

       

                                    图2-1 客户端连接服务器时可能抛出的异常

 

 

二. 获取Socket 的信息

 

    

      在一个Socket 对象中同时包含了远程服务器的IP 地址和端口信息, 以及客户本地的IP 地址和端口信息. 此外, 从Socket 对象中还可以获得输出流和输入流, 分别用于向服务器发送数据, 以及接收从服务器端发来的数据. 以下方法用于获取Socket的有关信息.

  • getInetAddress(): 获得远程服务器的IP 地址.
  • getPort(): 获得远程服务器的端口.
  • getLocalAddress(): 获得客户本地的IP 地址.
  • getLocalPort(): 获得客户本地的端口.
  • getInputStream(): 获得输入流. 如果Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownInput() 方法关闭输入流, 那么此方法会抛出IOException.
  • getOutputStream(): 获得输出流, 如果Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownOutput() 方法关闭输出流, 那么此方法会抛出IOException.

     这里有个HTTPClient 类的例子, 代码我是写好了, 也测试过了, 因为篇幅原因就不贴了. 这个HTTPClient 类用于访问网页 www./index.jsp. 该网页位于一个主机名(也叫域名)为 www. 的远程HTTP服务器上, 它监听 80 端口. 在HTTPClient 类中, 先创建了一个连接到该HTTP服务器的Socket对象, 然后发送符合HTTP 协议的请求, 接着接收从HTTP 服务器上发回的响应结果.

 

三. 关闭Socket

 

     当客户与服务器的通信结束, 应该及时关闭Socket , 以释放Socket 占用的包括端口在内的各种资源. Socket 的 close() 方法负责关闭Socket. 当一个Socket对象被关闭, 就不能再通过它的输入流和输出流进行I/O操作, 否则会导致IOException.

 

      为了确保关闭Socket 的操作总是被执行, 强烈建议把这个操作放在finally 代码块中:

 

   Socket socket = null;
   try{
        socket = new Socket(www.,80);
        //执行接收和发送数据的操作
        ..........
   }catch(IOException e){
         e.printStackTrace();
   }finally{
        try{
             if(socket != null)  socket.close();
       }catch(IOException e){e.printStackTrace();}
   }

 

   

    Socket 类提供了3 个状态测试方法.

  • isClosed(): 如果Socket已经连接到远程主机, 并且还没有关闭, 则返回true , 否则返回false .
  • isConnected(): 如果Socket曾经连接到远程主机, 则返回true , 否则返回false .
  • isBound(): 如果Socket已经与一个本地端口绑定, 则返回true , 否则返回false .

     如果要判断一个Socket 对象当前是否处于连接状态, 可采用以下方式:

 

      boolean isConnected = socket.isConnected() && !socket.isClosed();                                      

 

 

四. 半关闭Socket

 

     进程A 与进程B 通过Socket 通信, 假定进程A 输出数据, 进程B 读入数据. 进程A 如何告诉进程B 所有数据已经输出完毕? 下文略......

 

 

五. 设置Socket 的选项

 

     Socket 有以下几个选项.

  • TCP_NODELAY: 表示立即发送数据.
  • SO_RESUSEADDR: 表示是否允许重用Socket 所绑定的本地地址.
  • SO_TIMEOUT: 表示接收数据时的等待超时数据.
  • SO_LINGER: 表示当执行Socket 的 close()方法时, 是否立即关闭底层的Socket.
  • SO_SNFBUF: 表示发送数据的缓冲区的大小.
  • SO_RCVBUF: 表示接收数据的缓冲区的大小.
  • SO_KEEPALIVE: 表示对于长时间处于空闲状态的Socket , 是否要自动把它关闭.
  • OOBINLINE: 表示是否支持发送一个字节的TCP 紧急数据.

 

5.1 TCP_NODELAY 选项

  • 设置该选项: public void setTcpNoDelay(boolean on) throws SocketException
  • 读取该选项: public boolean getTcpNoDelay() throws SocketException

     默认情况下, 发送数据采用Negale 算法. Negale 算法是指发送方发送的数据不会立即发出, 而是先放在缓冲区, 等缓存区满了再发出. 发送完一批数据后, 会等待接收方对这批数据的回应, 然后再发送下一批数据. Negale 算法适用于发送方需要发送大批量数据, 并且接收方会及时作出回应的场合, 这种算法通过减少传输数据的次数来提高通信效率.

 

     如果发送方持续地发送小批量的数据, 并且接收方不一定会立即发送响应数据, 那么Negale 算法会使发送方运行很慢. 对于GUI 程序, 如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动), 这个问题尤其突出. 客户端鼠标位置改动的信息需要实时发送到服务器上, 由于Negale 算法采用缓冲, 大大减低了实时响应速度, 导致客户程序运行很慢.

 

      TCP_NODELAY 的默认值为 false, 表示采用 Negale 算法. 如果调用setTcpNoDelay(true)方法, 就会关闭 Socket的缓冲, 确保数据及时发送:

 

       if(!socket.getTcpNoDelay()) socket.setTcpNoDelay(true);                                                                                   

 

      如果Socket 的底层实现不支持TCP_NODELAY 选项, 那么getTcpNoDelay() 和 setTcpNoDelay 方法会抛出 SocketException.

 

 

5.2 SO_RESUSEADDR 选项

  • 设置该选项: public void setResuseAddress(boolean on) throws SocketException
  • 读取该选项: public boolean getResuseAddress() throws SocketException

     当接收方通过Socket 的close() 方法关闭Socket 时, 如果网络上还有发送到这个Socket 的数据, 那么底层的Socket 不会立即释放本地端口, 而是会等待一段时间, 确保接收到了网络上发送过来的延迟数据, 然后再释放端口.  Socket接收到延迟数据后, 不会对这些数据作任何处理. Socket 接收延迟数据的目的是, 确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到.

 

     客户程序一般采用随机端口, 因此出现两个客户程序绑定到同样端口的可能性不大. 许多服务器程序都使用固定的端口. 当服务器程序关闭后, 有可能它的端口还会被占用一段时间, 如果此时立刻在同一个主机上重启服务器程序, 由于端口已经被占用, 使得服务器程序无法绑定到该端口, 启动失败. (第三篇文章会对此作出介绍).

 

     为了确保一个进程关闭Socket 后, 即使它还没释放端口, 同一个主机上的其他进程还可以立即重用该端口, 可以调用Socket 的setResuseAddress(true) 方法:

 

        if(!socket.getResuseAddress()) socket.setResuseAddress(true);                                                                            

 

    值得注意的是 socket.setResuseAddress(true) 方法必须在 Socket 还没有绑定到一个本地端口之前调用, 否则执行 socket.setResuseAddress(true) 方法无效. 因此必须按照以下方式创建Socket 对象, 然后再连接远程服务器:

   

  Socket socket = new Socket();            //此时Socket对象未绑定本地端口,并且未连接远程服务器
  socket.setReuseAddress(true);
  SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
  socket.connect(remoteAddr);              //连接远程服务器, 并且绑定匿名的本地端口

 

    或者: 

 

  Socket socket = new Socket();              //此时Socke 对象为绑定本地端口, 并且未连接远程服务器
  socket.setReuseAddress(true);
  SocketAddress localAddr = new InetSocketAddress("localhost",9000);
  SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
  socket.bind(localAddr);             //与本地端口绑定
  socket.connect(remoteAddr); //连接远程服务器

 

    此外, 两个共用同一个端口的进程必须都调用 socket.setResuseAddress(true) 方法, 才能使得一个进程关闭 Socket后, 另一个进程的 Socket 能够立即重用相同端口.

 

 

 5.3 SO_TIMEOUT 选项

  • 设置该选项: public void setSoTimeout(int milliseconds) throws SocketException
  • 读取该选项: public int getSoTimeout() throws SocketException

     当通过Socket 的输入流读数据时, 如果还没有数据, 就会等待. 例如, 在以下代码中, in.read(buff) 方法从输入流中读入 1024个字节: 

 

  byte[] buff = new byte[1024];
  InputStream in = socket.getInputStream();
  in.read(buff); 

 

     如果输入流中没有数据, in.read(buff) 就会等待发送方发送数据, 直到满足以下情况才结束等待:

 

     略...............

 

     Socket 类的 SO_TIMEOUT 选项用于设定接收数据的等待超时时间, 单位为毫秒, 它的默认值为 0, 表示会无限等待, 永远不会超时. 以下代码把接收数据的等待超时时间设为 3 分钟:

 

        if(socket.getSoTimeout() == 0) socket.setSoTimeout(60000 * 3);   //注意, 原书中这里的代码错误, 里面的方法名字都少了"So"   

 

     Socket 的 setSoTimeout() 方法必须在接收数据之前执行才有效. 此外, 当输入流的 read()方法抛出 SocketTimeoutException 后, Socket 仍然是连接的, 可以尝试再次读数据:     

 

  socket.setSoTimeout(180000);
  byte[] buff = new byte[1024];
  InputStream in = socket.getInputStream();
  int len = -1;
  do{
   try{
    len = in.read(buff);
    //处理读到的数据
    //.........
   }catch(SocketTimeoutException e){
    //e.printStackTrace();
 
    System.out.println("等待读超时!");
    len = 0;
   }    
  }while(len != -1);

 

     例子ReceiveServer.java 和 SendClient.java 是一对简单的服务器/客户程序. sendClient 发送字符串 "hello everyone" ,接着睡眠 1 分钟, 然后关闭 Socket. ReceiveServer 读取 SendClient 发送来的数据, 直到抵达输入流的末尾, 最后打印 SendClient 发送来的数据.

 

     ReceiveServer.java 略....... ,         SendClient.java 略..........

 

     在 SendClient 发送字符串 "hello everyone" 后, 睡眠 1 分钟. 当 SendClient 在睡眠时, ReceiveServer 在执行 in.read(buff) 方法, 不能读到足够的数据填满 buff 缓冲区, 因此会一直等待 SendClient 发送数据. 如果在 ReceiveServer 类中 socket.setSoTimeout(20000) , 从而把等待接收数据的超时时间设为 20 秒, 那么 ReceiveServer 在等待数据时, 每当超过 20 秒, 就会抛出SocketTimeoutException . 等到 SendClient 睡眠 1 分钟后, SendClient 调用 Socket 的 close() 方法关闭 Socket, 这意味着 ReceiveServer 读到了输入流的末尾, ReceiveServer 立即结束读等待, read() 方法返回 -1 . ReceiveServer最后打印接收到的字符串 "hello everyone", 结果如下:

  等待读超时!
  等待读超时!
  hello everyone

5.4 SO_LINGER 选项

  • 设置该选项: public void setSoLinger(boolean on, int seconds) throws SocketException
  • 读取该选项: public int getSoLinger() throws SocketException

      SO_LINGER 选项用来控制 Socket 关闭时的行为. 默认情况下, 执行 Socket 的 close() 方法, 该方法会立即返回, 但底层的 Socket 实际上并不立即关闭, 它会延迟一段时间, 直到发送完所有剩余的数据, 才会真正关闭 Socket, 断开连接.

 

      如果执行以下方法:

 

      socket.setSoLinger(true, 0);                                                                                               

 

      那么执行Socket 的close() 方法, 该方法也会立即返回, 并且底层的 Socket 也会立即关闭, 所有未发送完的剩余数据被丢弃.

 

      如果执行以下方法:

 

      socket.setSoLinger(true, 3600);                                                                                           

 

      那么执行Socket 的 close() 方法, 该方法不会立即返回, 而是进入阻塞状态. 同时, 底层的 Socket 会尝试发送剩余的数据. 只有满足以下两个条件之一, close() 方法才返回:

      ⑴ 底层的 Socket 已经发送完所有的剩余数据;

 

      ⑵ 尽管底层的 Socket 还没有发送完所有的剩余数据, 但已经阻塞了 3600 秒(注意这里是秒, 而非毫秒), close() 方法的阻塞时间超过 3600 秒, 也会返回, 剩余未发送的数据被丢弃.

 

      值得注意的是, 在以上两种情况内, 当close() 方法返回后, 底层的 Socket 会被关闭, 断开连接. 此外, setSoLinger(boolean on, int seconds) 方法中的 seconds 参数以秒为单位, 而不是以毫秒为单位.    

 

      如果未设置 SO_LINGER 选项, getSoLinger()  返回的结果是 -1, 如果设置了 socket.setSoLinger(true, 80) , getSoLinger()  返回的结果是 80.

 

Tips: 当程序通过输出流写数据时, 仅仅表示程序向网络提交了一批数据, 由网络负责输送到接收方. 当程序关闭 Socket, 有可能这批数据还在网络上传输, 还未到达接收方. 这里所说的 "未发送完的数据" 就是指这种还在网络上传输, 未被接收方接收的数据.

 

    例子 SimpleClient.java 与 SimpleServer.java 所示是一对简单的客户/服务器程序. SimpleClient 类发送一万个字符给 SimpleServer, 然后调用Socket 的 close() 方法关闭 Socket.

 

    SimpleServer 通过 ServerSocket 的 accept() 方法接受了 SimpleClient 的连接请求后,  并不立即接收客户发送的数据, 而是睡眠 5 秒钟后再接收数据. 等到 SimpleServer 开始接收数据时, SimpleClient 有可能已经执行了 Socket 的close() 方法, 那么 SimpleServer 还能接收到 SimpleClient 发送的数据吗?

 

    SimpleClient.java 略..., SimpleServer.java 略......

 

    SimpleClient.java中

  System.out.println("开始关闭 Socket");
  long begin = System.currentTimeMillis();
  socket.close();
  long end = System.currentTimeMillis();
  System.out.println("关闭Socket 所用的时间为:" + (end - begin) + "ms");

 

    下面分 3 种情况演示 SimpleClient 关闭 Socket 的行为.  

    ⑴ 未设置 SO_LINGER 选项, 当 SimpleClient 执行 Socket 的close() 方法时, 立即返回, SimpleClient 的打印结果如下:

 

  开始关闭 Socket
  关闭Socket 所用的时间为:0ms

 

     等到 SimpleClient 结束运行, SimpleServer 可能才刚刚结束睡眠, 开始接收 SimpleClient 发送的数据. 此时尽管 SimpleClient 已经执行了 Socket 的 close() 方法, 并且 SimpleClient 程序本身也运行结束了, 但从 SimpleServer 的打印结果可以看出, SimpleServer 仍然接收到了所有的数据. 之所以出现这种情况, 是因为当 SimpleClient 执行了 Socket 的 close() 方法后, 底层的 Socket 实际上并没有真正关闭, 与 SimpleServer 的连接依然存在. 底层的 Socket 会存在一段时间, 直到发送完所有的数据.

 

     ⑵ 设置SO_LINGER 选项, socket.setSoLinger(true, 0). 这次当 SimpleClient 执行 Socket 的 close() 方法时, 会强行关闭底层的 Socket, 所有未发送完的数据丢失. SimpleClient 的打印结果如下:

 

  开始关闭 Socket
  关闭Socket 所用的时间为:0ms

 

     从打印结果看出, SimpleClient 执行 Socket 的 close() 方法时, 也立即返回. 当 SimpleServer 结束睡眠, 开始接收 SimpleClient 发送的数据时, 由于 SimpleClient 已经关闭底层 Socket, 断开连接, 因此 SimpleServer 在读数据时会抛出 SocketException:

 

        java.net.SocketException: Connection reset                                        

 

     ⑶ 设置SO_LINGER 选项, socket.setSoLinger(true, 3600). 这次当 SimpleClient 执行 Socket 的close() 方法时, 会进入阻塞状态, 知道等待了 3600 秒, 或者底层 Socket 已经把所有未发送的剩余数据发送完毕, 才会从 close() 方法返回. SimpleClient 的打印结果如下:

 

  开始关闭 Socket
  关闭Socket 所用的时间为:5648ms

 

     当 SimpleServer 结束了 5 秒钟的睡眠, 开始接收 SimpleClient 发送的数据时, SimpleClient 还在这些 Socket 的close() 方法, 并且处于阻塞状态. SimpleClient 与 SimpleServer 之间的连接依然存在, 因此 SimpleServer 能够接收到 SimpleClient 发送的所有数据.

 

5.5 SO_RCVBUF 选项

  • 设置该选项: public void setReceiveBufferSize(int size) throws SocketException
  • 读取该选项: public int getReceiveBufferSize() throws SocketException

     SO_RCVBUF 表示 Socket 的用于输入数据的缓冲区的大小. 一般说来, 传输大的连续的数据块(基于HTTP 或 FTP 协议的通信) 可以使用较大的缓冲区, 这可以减少传输数据的次数, 提高传输数据的效率. 而对于交互频繁且单次传送数据量比较小的通信方式(Telnet 和 网络游戏), 则应该采用小的缓冲区, 确保小批量的数据能及时发送给对方. 这种设定缓冲区大小的原则也同样适用于 Socket 的 SO_SNDBUF 选项.

 

      如果底层 Socket 不支持 SO_RCVBUF 选项, 那么 setReceiveBufferSize() 方法会抛出 SocketException.

 

5.6 SO_SNDBUF 选项

  • 设置该选项: public void setSendBufferSize(int size) throws SocketException
  • 读取该选项: public int getSendBufferSize() throws SocketException

     SO_SNDBUF 表示 Socket 的用于输出数据的缓冲区的大小. 如果底层 Socket 不支持 SO_SNDBUF 选项, setSendBufferSize() 方法会抛出 SocketException.

 

5.7 SO_KEEPALIVE 选项

  • 设置该选项: public void setKeepAlive(boolean on) throws SocketException
  • 读取该选项: public boolean getKeepAlive() throws SocketException //原书中这个方法返回的类型是int

      当 SO_KEEPALIVE 选项为 true 时, 表示底层的TCP 实现会监视该连接是否有效. 当连接处于空闲状态(连接的两端没有互相传送数据) 超过了 2 小时时, 本地的TCP 实现会发送一个数据包给远程的 Socket. 如果远程Socket 没有发回响应, TCP实现就会持续尝试 11 分钟, 直到接收到响应为止. 如果在 12 分钟内未收到响应, TCP 实现就会自动关闭本地Socket, 断开连接. 在不同的网络平台上, TCP实现尝试与远程Socket 对话的时限有所差别.

 

      SO_KEEPALIVE 选项的默认值为 false, 表示TCP 不会监视连接是否有效, 不活动的客户端可能会永远存在下去, 而不会注意到服务器已经崩溃.

 

      以下代码把 SO_KEEPALIVE 选项设为 true:

 

        if(!socket.getKeepAlive()) socket.setKeepAlive(true);                                                              

 

5.8 OOBINLINE 选项

  • 设置该选项: public void setOOBInline(boolean on) throws SocketException
  • 读取该选项: public boolean getOOBInline() throws SocketException  //原书中这个方法返回的类型是int

     当 OOBINLINE 为 true 时, 表示支持发送一个字节的 TCP 紧急数据. Socket 类的 sendUrgentData(int data) 方法用于发送一个字节的 TCP紧急数据.

 

     OOBINLINE 的默认值为 false, 在这种情况下, 当接收方收到紧急数据时不作任何处理, 直接将其丢弃. 如果用户希望发送紧急数据, 应该把 OOBINLINE 设为 true:

 

          socket.setOOBInline(true);                                                             

 

      此时接收方会把接收到的紧急数据与普通数据放在同样的队列中. 值得注意的是, 除非使用一些更高层次的协议, 否则接收方处理紧急数据的能力有限, 当紧急数据到来时, 接收方不会得到任何通知, 因此接收方很难区分普通数据与紧急数据, 只好按照同样的方式处理它们.

 

 

5.9 服务类型选项

 

      当用户通过邮局发送普通信、挂号信或快件时, 实际上是选择了邮局提供的不同的服务.  发送普通信的价格最低, 但发送速度慢, 并且可靠性没有保证. 发送挂号信的价格稍高,  但可靠性有保证. 发送快件的价格最高, 发送速度最快, 并且可靠性有保证.

 

      在 Internet 上传输数据也分为不同的服务类型, 它们有不同的定价. 用户可以根据自己的需求, 选择不同的服务类型. 例如, 发送视频需要较高的带宽, 快速到达目的地, 以保证接收方看到连续的画面. 而发送电子邮件可以使用较低的带宽, 延迟几个小时到达目的地也没有关系.

 

      IP 规定了 4 种服务类型, 用来定性地描述服务的质量.

  • 低成本: 发送成本低.
  • 高可靠性: 保证把数据可靠地送达目的地.
  • 最高吞吐量: 一次可以接收或发送大批量的数据.
  • 最小延迟: 传输数据的速度快, 把数据快速送达目的地.

      这 4 种服务类型还可以进行组合. 例如, 可以同时要求获得高可靠性和最小延迟.

 

      Socket 类中提供了设置和读取服务类型的方法.

  • 设置服务类型: public void setTrafficClass(int trafficClass) throws SocketException
  • 读取服务类型: public int getTrafficClass() throws SocketException

      Socket 类用 4 个整数表示服务类型.

  • 低成本: 0x02 (二进制的倒数第二位为1)
  • 高可靠性: 0x04 (二进制的倒数第三位为1)
  • 最高吞吐量: 0x08 (二进制的倒数第四位为1)
  • 最小延迟: 0x10 (二进制的倒数第五位为1)

      例如, 以下代码请求高可靠性传输服务:

  socket = new Socket(host, port);
  socket.setTrafficClass(0x04);

 

      再例如, 以下代码请求高可靠性和最小延迟传输服务:

socket.setTrafficClass(0x04|0x10);        //把 0x04 与 0x10 进行位或运算     

 

5.10 设定连接时间、延迟和带宽的相对重要性

 

      在 JDK 1.5 中, 还为 Socket 类提供了一个 setPerformancePreferences()  方法:

        public void setPerformancePreferences(int connectionTime, int latency, int bandwidth)               

 

      以上方法的 3 个参数表示网络传输数据的 3 选指标.

  • 参数 connectionTime: 表示用最少时间建立连接.
  • 参数 latency: 表示最小延迟.
  • 参数 bandwidth: 表示最高带宽.

      setPerformancePreferences() 方法用来设定这 3 项指标之间的相对重要性. 可以为这些参数赋予任意的整数, 这些整数之间的相对大小就决定了相应参数的相对重要性.

 

      例如, 如果参数 connectionTime 为 2, 参数 latency 为 1, 而参数bandwidth 为 3, 就表示最高带宽最重要, 其次是最少连接时间, 最后是最小延迟.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多