Java Socket 学习笔记
TCP协议的Socket编程
Socket:英文中的意思是插座。两个Java应用程序可以通过一个双向的网络通信连接实现数据交换,这个双向链路的一端称为一个Socket。Java中所有关于网络编程的类都位于java.net包。
Socket用法详解
TCP编程需要使用的两个类:Socket类与ServerSocket类,分别用来实现双向连接的Client端和Server端。
说明:这里仅指TCP连接。因为UDP中没有Client和Server的概念。UDP只负责发送,不管是否发生成功。
建立连接时所需要的寻址信息为远程计算机的IP地址和端口号。
端口
一个端口号只能被一个应用程序占用。
1024以下的端口号都由系统使用。普通应用程序不应该去占用。比较有名的应用程序占用某个特定的端口号,一般也不该去占用。如HTTP端口,80端口。FTP,21端口。SMTP,25端口。接受邮件,110端口。
说明:TCP端口与UDP端口是两个不同的概念。TCP端口有65535个端口,UDP端口也有65536个端口。TCP的8888端口与UDP的8888端口是两个不同的概念。
ServerSocket的构造函数
ServerSocket() // 创建非绑定服务器套接字。 ServerSocket(int port) // 创建绑定到特定端口的服务器套接字。
ServerSocket常用API
Socket accept() // 侦听并接受到此套接字的连接。 阻塞方法,当没有接收到新的网络连接前是不会继续执行的。
Socket的构造函数
Socket() // 通过系统默认类型的 SocketImpl 创建未连接套接字 Socket(InetAddress address, int port) // 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 Socket(InetAddress address, int port, InetAddress localAddr, int localPort) // 创建一个套接字并将其连接到指定远程地址上的指定远程端口。 Socket(Proxy proxy) // 创建一个未连接的套接字并指定代理类型(如果有),该代理不管其他设置如何都应被使用。 Socket(String host, int port) // 创建一个流套接字并将其连接到指定主机上的指定端口号。 Socket(String host, int port, InetAddress localAddr, int localPort) // 创建一个套接字并将其连接到指定远程主机上的指定远程端口。
连接服务器并设定超时时间
客户端的socket建立连接需要一定的时间完成。默认情况下,socket会一直等待下去,或抛出异常。
void connect(SocketAddress endpoint) // 将此套接字连接到服务器。 void connect(SocketAddress endpoint, int timeout) // 将此套接字连接到服务器,并指定一个超时值。timeout的单位为ms。若timeout为0,则永不超时。
客户端连接服务器可能抛出异常
- UnknownHostException 无法识别主机的名字或IP地址。
- ConnectException 没有服务器进程指定的端口,或者服务器进程拒绝连接。
- SocketTimeoutException 等待超时
- BindException 无法把socket对象与指定的本地IP或端口绑定。
关闭Socket
当客户与服务器的通信结束,应该及时关闭Socket,以释放Socket占用的各种资源。当Socket对象被关闭,则无法再使用IO操作,否则引发IOException。
使用如下方法关闭套接字:
void close() // 关闭此套接字。
半关闭Socket
进程A与进程B通过Socket通信,假定进程A输出数据,进程B读入数据。进程A如何告诉进程B数据已经输出完毕呢?
方法一:若进程A与进程B交换的是字符串,并都一行一行的读/写数据时。可以使进程A与进程B交换预先约定,以一个特殊标志作为结束标志,如字符串bye。
方法二:进程A发送一个消息,告诉进程B所发送的正文的长度,然后再发送正文。进程B获知进程A将发送的正文的长度,接下来只要读取完该长度的字符或字节,就停止读数据。
方法三:进程A发完所有数据后,关闭socket。当进程B读入进程A发送的所有数据后,再次执行输入流的read()方法时,该方法返回-1。若执行BufferedReader的readLine()方法,那么该方法返回null。
方法四:当调用Socket的close()方法关闭Socket时候,它的输出流和输入流也被关闭。有的时候,可能仅仅希望关闭输出流或输入流之一。此时可以采用Socket类提供的半关闭方法。
void shutdownInput() 此套接字的输入流置于“流的末尾”。
void shutdownOutput() 禁用此套接字的输出流。
进程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紧急数据。 |
TCP_NODELAY
void setTcpNoDelay(boolean on) // 启用/禁用 TCP_NODELAY(启用/禁用 Nagle 算法)。 boolean getTcpNoDelay() // 测试是否启用 TCP_NODELAY。
默认情况下,发送数据采用Negale算法。Negale算法是指发送方发送的数据不会立刻发出,而是先放在缓冲区内,等缓冲区满了在发出。发送完一批数据后,会等待接收方对这批数据的回应,然后在发送下一批数据。Negale算法适用于发送方需要发送大批量数据,并且接收方会及时作出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。
若发送方持续地发送小批量数据,并且接收方不一定会立即发送响应数据,那么Negale算法会使发送方运行很慢。对于GUI程序,如网络游戏程序(服务器需要实时游戏数据发送到服务器),采用Negale算法采用缓冲,大大降低了实时响应速度,导致客户程序运行很慢。
TCP_NODEALY的默认值为false,表示采用Negale算法。若调用setTcpNoDelay(true)方法,就会关闭socket的缓冲,确保数据及时发送。
若socket的底层不支持TCP_NODELAY选项,那么getTcpNoDelay()和setTcpNoDelay()方法会排出SocketException。
SO_REUSEADDR
public void setReuseAddress(boolean on) throws SocketException public boolean getReuseAddress() throws SocketException
当接收方通过Socket的close()方法关闭socket时,若网络上还有发送到这个socket的数据,那么底层的socket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。Socket接收到延迟数据后,不会对这些数据作任何处理。Socket接收延迟数据的目的是,确保这些数据不会被其他碰巧绑定到童谣端口的新进程接收到。
客户端一般采用随机端口,一次出现两个客户程序绑定到同样端口的可能性不大。许多服务器程序都使用固定端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,若此时立刻在同一个主机上重启服务器程序后,有可能它的端口还会被占用一段时间,若此时立刻在同一个主机上充气服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,启动失败。
为了确保一个惊喜关闭socket后,即使它还没释放端口,同一个主机上的其他进程还可以了可重用该端口,可以调用Socket的setReuseAddress(true)方法。
if(!socket.getResuseAddress()) socket.setResuseAddress(true);
注意:socket.setResuseAddress(true)方法必须在Socket还没有绑定到一个本地端口之前调用,否则执行socket.setResuseAddress(true)方法无效。因此必须按照以下方式创建Socket对象,然后再连接远程服务器:
Socket socket = new Socket(); //此时Socket对象未绑定到本地端口,并且未连接远程服务器
socket.setResuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
socket.connect(remoteAddr); //连接远程服务器,并且绑定匿名的本地端口
或者:
Socket socket = new Socket(); //此时Socket对象未绑定到本地端口,并且未连接远程服务器
socket.setResuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
socket.bind(localAddr); //与本地端口绑定
socket.connect(remoteAddr); //连接远程服务器,并且绑定匿名的本地端口
两个公用同一端口的进程必须都调用socket.setResuseAddress(true)方法,才能使得一个进程关闭Socket后,另一个进程的Socket能够立刻重用相同端口。
SO_TIMEOUT选项
public void setSoTimeout(int milliseconds) throws SocketException public int getSoTimeOut() throws SocketException
当通过Socket的输入流读数据时,若还没有数据,就会等待。
Socket类的SO_TIMEOUT选项用于设定接收数据的等待超时时间,单位为毫秒,它的默认值为0,表示会无限等待,永远不会超时。
Socket的setSoTimeout()方法必须在接收数据之前执行才有效。此外,当输入流的read()方法抛出SocketTimeoutException后,Socket仍然是连接的,可以尝试再次读取数据。
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 second)方法中的seconds参数以秒为单位,而不是以毫秒为单位。
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。
SO_SNDBUF选项
public void setSendBufferSize(int size) throws SocketException public int getSendBufferSize() throws SocketException
SO_SNDBUF表示Socket的用于输出数据的缓冲区的大小。如果底层Socket不支持SO_SNDBUF选项,setSendBufferSize()方法会抛出SocketException。
SO_KEEPALIVE选项
public void setKeepAlive(boolean on) throws SocketException public int getKeepAlive() throws SocketException
当SO_KEEPALIVE选项为true,表示底层的TCP实现会监视该连接是否有效。
当连接处于空闲状态(连接的两端没有相互传送数据)超过了2小时,本地TCP实现会发送一个数据包给远程的Socket。若远程Socket没有发回响应,TCP实现就会持续尝试11分钟,直到接收到响应为止。若12分钟内未接收到响应,TCP实现就会自动关闭本地Soclet,断开连接。在不同的网络平台上,TCP实现尝试与远程Socket对话的时限会有所差别。
SO_KEEPALIVE选项的默认值为false,表示TCP不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃。
OOBINLINE选项
public void setOOBInline(int size) throws SocketException public int getOOBInline () throws SocketException
当OOBINLINE为true时,表示支持发送一个字节的TCP紧急数据。Socket类的sendUrgentDate(int data)方法用于发送一个字节的TCP紧急数据。
OOBINLINE的默认值为false,在这种情况下,当接收方收到紧急数据时不作任何处理,直接将其丢弃。如果用户希望发送紧急数据,应该把OOBINLINE设为true:socket.setOOBInline(true); 此时接收方会把接收到的紧急数据与普通数据放在同样的队列中。值得注意的是,除非使用一些更高层次的协议,否则接收方处理紧急数据的能力非常有限,当紧急数据到来时,接收方不会得到任何通知,因此接收方很难区分普通数据与紧急数据,只好按照同样的方式处理它们。
检测Socket状态
boolean isClosed() // 返回套接字的关闭状态。 boolean isConnected() // 返回套接字的连接状态。 boolean isBound() // 返回套接字的绑定状态。 boolean isConnected() // 返回套接字的连接状态。 boolean isInputShutdown() // 返回是否关闭套接字连接的半读状态 (read-half)。 boolean isOutputShutdown() // 返回是否关闭套接字连接的半写状态 (write-half)。
服务类型选项
在Internet上传输数据也分为不同的服务类型,他们有不同的定价。用户可以根据自己的需求,选择不同的服务类型。例如,发送视频需要较高的贷款,快速到达目的地,以保证接收方看到连续的服务类型。例如,发送视频需要较高的带宽,快速到达目的地,以保证接收方看到连续的画面。而电子邮件可以使用较低的贷款,延迟几个小时到达目的地也没关系。
Socket类中提供了设置和读取服务类型的方法:
public void setTrafficClass(int trafficClass) throws SocketException public int getTrafficClass() throws SocketException
IP规定4种服务类型,用来定性地描述服务的质量。这四种服务类型还可以进行组合。例如,可以同时要求获得高可靠性和最小延迟。
服务类型 | 数值 | 说明 |
低成本 | 0x02 = 00010 = 2 | 发送成本低。 |
高可靠性 | 0x04 = 00100 = 4 | 保证把数据可靠的送达目的地。 |
最高吞吐量 | 0x08 = 01000 = 8 | 一次可以接收或发送大批量的数据。 |
最小延迟 | 0x10 = 10000 = 16 | 传输数据的速度快,把数据快速送达目的地。 |
设定连接时间、延迟和带宽的相对重要性
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
以上方法的三个参数表示网络传输数据的三项指标:
- 参数connectionTime:表示用最少时间建立连接。
- 参数latency:表示最小延迟。
- 参数bandwidth:表示最高带宽。
setPerformancePreferences()方法用来设定这三项指标之间的相对重要性。可以为这些参数赋予任意的整数,这些整数之间的相对大小就决定了相应参数的相对重要性。例如,如果参数connectionTime为2,参数latency为1,而参数bandwidth为3,就表示最高带宽最重要,其次是最少连接时间,最后是最小延迟。
二、ServerSocket用法详解
在客户/服务器通信模式中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户连接请求。
构造ServerSocket
ServerSocket的构造方法有以下几种重载形式:
ServerSocket() throws IOException ServerSocket(int port) throws IOException ServerSocket(int port, int backlog) throws IOException ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上构造方法中:
- 参数port指定服务器要绑定的端口(服务器要监听的端口)。
- 参数backlog指定客户连接请求队列的长度。
- 参数bindAddr指定服务器要绑定的IP地址。
绑定端口
除了第一个不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端口由参数port指定。例如,以下代码创建了一个与80端口绑定的服务器:
ServerSocket serverSocket = new ServerSocket(80);
如果运行时无法绑定到80端口,以上代码会抛出IOException,更确切地说,是抛出BindException,它是IOException的子类。BindException一般是由以下原因造成的:
端口已经被其他服务器进程占用;在某些操作系统中,如果没有以超级用户的身份来运行服务器程序,那么操作系统不允许服务器绑定到1~1023之间的端口。
如果把参数port设为0,表示由操作系统来为服务器分配一个任意可用的端口。由操作系统分配的端口也称为匿名端口。对于多数服务器,会使用明确的端口,而不会使用匿名端口,因为客户程序需要事先知道服务器的端口,才能方便地访问服务器。在某些场合,匿名端口有着特殊的用途。
设定客户连接请求队列的长度
当服务器进程运行时,可能会同时监听到多个客户的连接请求。例如,每当一个客户进程执行以下代码:
Socket socket = new Socket("www.baidu.com",80);
就意味着在远程www.baidu.com主机的80端口上,监听到了一个客户的连接请求。管理客户连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。当最大容当队列中的连接请求达到了队列量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。
ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。
注意:,在以下几种情况中,仍然会采用操作系统限定的队列的最大长度:
backlog参数的值大于操作系统限定的队列的最大长度;
backlog参数的值<=0;
在ServerSocket构造方法中没有设置backlog参数。
设定绑定的IP地址
如果主机只有一个IP地址,那么默认情况下,服务器程序就与该IP地址绑定。ServerSocket的第4个构造方法ServerSocket(int port, int backlog, InetAddress bindAddr)有一个bindAddr参数,它显式指定服务器要绑定的IP地址,该构造方法适用于具有多个IP地址的主机。假定一个主机有两个网卡,一个网卡用于连接到Internet, IP地址为222.67.5.94,还有一个网卡用于连接到本地局域网,IP地址为192.168.3.4。如果服务器仅仅被本地局域网中的客户访问,那么可以按如下方式创建ServerSocket:
ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.3.4"));
默认构造方法的作用
ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。
这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。
在以下代码中,先把ServerSocket的SO_REUSEADDR选项设为true,然后再把它与8000端口绑定:
ServerSocket serverSocket=new ServerSocket(); serverSocket.setReuseAddress(true); //设置ServerSocket的选项 serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
错误的设置方法:把以上程序代码改为如下形式:
ServerSocket serverSocket=new ServerSocket(8000); serverSocket.setReuseAddress(true); //设置ServerSocket的选项
则serverSocket.setReuseAddress(true)方法不起任何作用,SO_ REUSEADDR选项必须在服务器绑定端口之前设置才有效。
接收和关闭与客户的连接
ServerSocket的accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。
接下来,服务器从Socket对象中获得输入流和输出流,就能与客户交换数据。当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:
java.net.SocketException: Connection reset by peer
这只是服务器与单个客户通信中出现的异常,这种异常应该被捕获,使得服务器能继续与其他客户通信。
以下程序显示了单线程服务器采用的通信流程:
public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //从连接请求队列中取出一个连接 System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); // 接收和发送数据 // … } catch (IOException e) { //这只是与单个客户通信时遇到的异常,可能是由于客户端过早断开连接引起的 //这种异常不应该中断整个while循环 e.printStackTrace(); } finally { try { if(socket!=null) socket.close(); //与一个客户通信结束后,要关闭Socket } catch (IOException e) {e.printStackTrace();} } } }
与单个客户通信的代码放在一个try代码块中,如果遇到异常,该异常被catch代码块捕获。try代码块后面还有一个finally代码块,它保证不管与客户通信正常结束还是异常结束,最后都会关闭Socket,断开与这个客户的连接。
关闭ServerSocket
ServerSocket的close()方法使服务器释放占用的端口,并且断开与所有客户的连接。当一个服务器程序运行结束时,即使没有执行ServerSocket的close()方法,操作系统也会释放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行ServerSocket的close()方法。
在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。
例如,以下代码用于扫描1~65535之间的端口号。如果ServerSocket成功创建,意味着该端口未被其他服务器进程绑定,否者说明该端口已经被其他进程占用:
for (int port=1;port<=65535;port++) { try{ ServerSocket serverSocket=new ServerSocket(port); serverSocket.close(); //及时关闭ServerSocket } catch(IOException e) { System.out.println("端口"+port+" 已经被其他服务器进程占用"); } }
以上程序代码创建了一个ServerSocket对象后,就马上关闭它,以便及时释放它占用的端口,从而避免程序临时占用系统的大多数端口。
ServerSocket的isClosed()方法判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false。
ServerSocket的isBound()方法判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true。
如果需要确定一个ServerSocket已经与特定端口绑定,并且还没有被关闭,则可以采用以下方式:
boolean isOpen=serverSocket.isBound() && !serverSocket.isClosed();
获取ServerSocket的信息
ServerSocket的以下两个get方法可分别获得服务器绑定的IP地址,以及绑定的端口:
public InetAddress getInetAddress() public int getLocalPort()
匿名端口
在构造ServerSocket时,如果把端口设为0,那么将由操作系统为服务器随机分配一个端口(称为匿名端口),程序只要调用getLocalPort()方法就能获知这个端口号。
多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放。例如,FTP协议就使用了匿名端口。
例:为服务器程序随机分配可用端口(匿名端口)。
import java.io.*; import java.net.*; public class RandomPort{ public static void main(String args[])throws IOException{ ServerSocket serverSocket = new ServerSocket(0); System.out.println("监听的端口为:" + serverSocket.getLocalPort()); } }
多次运行RandomPort程序,可能会得到如下运行结果:
监听的端口为:3000 监听的端口为:3004 监听的端口为:3005
ServerSocket选项
ServerSocket有以下3个选项:
选项 | 说明 |
SO_TIMEOUT | 表示等待客户连接的超时时间。 |
SO_REUSEADDR | 表示是否允许重用服务器所绑定的地址。 |
SO_RCVBUF | 表示接收数据的缓冲区的大小。 |
SO_TIMEOUT选项
public void setSoTimeout(int timeout) throws SocketException public int getSoTimeout () throws IOException
SO_TIMEOUT表示ServerSocket的accept()方法等待客户连接的超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。
如果把程序中的“serverSocket.setSoTimeout(6000)”注释掉,那么serverSocket. accept()方法永远不会超时,它会一直等待下去,直到接收到了客户的连接,才会从accept()方法返回。
Tips:服务器执行serverSocket.accept()方法时,等待客户连接的过程也称为阻塞。
SO_REUSEADDR选项
public void setReuseAddress(boolean on) throws SocketException public boolean getReuseAddress() throws SocketException
这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。
当ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。
许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException:
Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind
为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket的setResuseAddress(true)方法:
if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(true);
值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相同端口。
SO_RCVBUF选项
public void setReceiveBufferSize(int size) throws SocketException public int getReceiveBufferSize() throws SocketException
SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
SO_RCVBUF的默认值与操作系统有关。例如,在Windows 2000中运行以下代码时,显示SO_RCVBUF的默认值为8192:
ServerSocket serverSocket = new ServerSocket(8000); System.out.println(serverSocket.getReceiveBufferSize()); // 打印8192
无论在ServerSocket绑定到特定端口之前或之后,调用setReceiveBufferSize()方法都有效。例外情况下是如果要设置大于64K的缓冲区,则必须在ServerSocket绑定到特定端口之前进行设置才有效。例如,以下代码把缓冲区设为128K:
ServerSocket serverSocket = new ServerSocket(); int size=serverSocket.getReceiveBufferSize(); if(size<131072) serverSocket.setReceiveBufferSize(131072); // 把缓冲区的大小设为128K serverSocket.bind(new InetSocketAddress(8000)); // 与8000端口绑定
执行serverSocket.setReceiveBufferSize()方法,相当于对所有由serverSocket.accept()方法返回的Socket设置接收数据的缓冲区的大小。
设定连接时间、延迟和带宽的相对重要性
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。
例:简单的Client-Server。
TCP的Server端
import java.net.*; import java.io.*; public class TCPServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(6666); // 建立监听 while(true) { Socket s = ss.accept(); // 等待请求。若请求,则创建Socket,即建立连接。 System.out.println("a client connect!"); // 输入流 DataInputStream dis = new DataInputStream(s.getInputStream()); System.out.println(dis.readUTF()); // 从流中读出信息。 dis.close(); // 关闭流。 s.close(); // 关闭socket。 } } }
TCP的Client端
import java.net.*; import java.io.*; public class TCPClient { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1", 6666); // 建立Socket,寻址,申请连接。 OutputStream os = s.getOutputStream(); // 使用流做通信。输出流 DataOutputStream dos = new DataOutputStream(os); Thread.sleep(30000); dos.writeUTF("hello server!"); // 以UTF-8的编码将数据写入流。 dos.flush(); // 压出流中所有信息。 dos.close(); // 关闭流。 s.close(); // 关闭socket。 } }
说明:
- 网络应用程序的编程过程中,应该先启动Server再启动Client。
- ServerSocket类的accept()方法是阻塞式的方法,即若没有接受到连接,则当前线程被阻塞,不再继续执行。
- 程序中使用readUTF()方法也是阻塞式的方法。
- Client端的端口是系统自动选择的,我们不需要去关注。
该示例程序的缺点:
- 由于Server端的readUTF()方法是阻塞式的。Server需要等待Client发送数据才会继续执行,所以将会导致accept()方法无法执行。
- 由于readUTF方法是阻塞方式的。所以如果read()和write()方法是在同一个处理过程中,必须是一段是先读后写,另一端是先写后读。若两个都是先后后写,则Server端和Client都不会继续执行。
InetAddress:IP地址的超集。除了IP地址。还可以表示其他通信协议的地址。
创建多线程的服务器
许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。
可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:
- 能同时接收并处理多个客户连接;
- 对于每个客户,都会迅速给予响应。
服务器同时处理的客户连接数目越多,并且对每个客户作出响应的速度越快,就表明并发性能越高。用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。
可以通过使用线程池来降低创建线程和释放线程所占用的系统资源与运行时间。
服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。
以下是EchoServer的service()方法的代码:
public void service() { while (true) { Socket socket = null; try { socket = serverSocket.accept(); // 接收客户连接 Thread workThread = new Thread(new Handler(socket)); // 创建一个工作线程。 workThread.start(); // 启动工作线程 }catch (IOException e) { e.printStackTrace(); } } }
以上工作线程workThread执行Handler的run()方法。Handler类实现了Runnable接口,它的run()方法负责与单个客户通信,与客户通信结束后,就会断开连接,执行Handler的run()方法的工作线程也会自然终止。
例:服务器为每一个接收到的客户端socket分配一个线程。
import java.io.*; import java.net.*; public class EchoServer { private int port=8000; private ServerSocket serverSocket; public EchoServer() throws IOException { serverSocket = new ServerSocket(port); // 服务器启动 } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); // 接收客户连接 Thread workThread=new Thread(new Handler(socket)); // 创建一个工作线程。把接受到的socket传入处理。 workThread.start(); // 启动工作线程 }catch (IOException e) { e.printStackTrace(); } } } public static void main(String args[])throws IOException { new EchoServer().service(); } } // 线程处理器类,负责与单个客户的通信。 class Handler implements Runnable{ private Socket socket; public Handler(Socket socket){ this.socket=socket; } private PrintWriter getWriter(Socket socket) throws IOException {…} private BufferedReader getReader(Socket socket) throws IOException {…} public String echo(String msg) {…} public void run(){ try { System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); BufferedReader br =getReader(socket); PrintWriter pw = getWriter(socket); String msg = null; while ((msg = br.readLine()) != null) { // 接收和发送数据,直到通信结束。 System.out.println(msg); pw.println(echo(msg)); if (msg.equals("bye")) { break; } } } catch (IOException e) { e.printStackTrace(); } finally { try{ if(socket!=null) socket.close(); // 断开连接 }catch (IOException e) {e.printStackTrace();} } } }
UDP协议的Socket编程
UDP是不可靠的。UDP是基于数据报的形式发送数据。(TCP也本质也以数据报发送,但安全性强。)
UDP协议传输其实没有Server与Client的概念。
UDP编程中最重要的两个类:
- DatagramPacket类:表示数据报,用于将封装byte数组中的数据。
- DatagramSocket类:负责接收和发送数据报。
为了发送数据需要将数据放到DatagramPacket中,使用DatagramSocket发送该包。为了接受数据,要从DatagramSocket中接收一个DatagramPacket对象,然后读取该包中的内容。
一般实际编程中DatagramPacket的缓冲区大小设置不要超过8192字节。许多操作系统不支持超过8KB的数据报,否则就会有更大的数据报截断、分片或丢弃。如果数据报太大,而导致网络将其截断或丢弃,Java程序将得不到任何通知。
例:基于UDP的客户端与服务器端程序编程。
使用UDP传输的Server端
import java.net.*; import java.io.*; public class TestUDPServer { public static void main(String args[]) throws Exception { byte buf[] = new byte[1024]; // buf相当于缓冲区。大小为1024。 //创建数据报。 DatagramPacket dp = new DatagramPacket(buf, buf.length); //该数据报与字节数组关联。 DatagramSocket ds = new DatagramSocket(5678); //占用5678端口。 while(true) { ds.receive(dp); // 接收数据报,阻塞方法。把数据报传递至dp,即数据报中的数据存入buf。 ByteArrayInputStream bais = new ByteArrayInputStream(buf); //从字节数组(源)接上读取流。 DataInputStream dis = new DataInputStream(bais); //用Data流包装读取流。 System.out.println(dis.readLong()); //从Data流中读取数据。 } } }
使用UDP传输的Client端
import java.net.*; import java.io.*; public class TestUDPClient { public static void main(String args[]) throws Exception { // 首先将一个long数,装为字节数组。 long n = 10000L; ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 创建写入内存的流(字节数组流)。该流用于向内存写数据。 DataOutputStream dos = new DataOutputStream(baos); // 用数据流包装内存流。 dos.writeLong(n); // 向数据流中写入long数。数据流负责把任何类型的数据写到内存流。通过内存流数据写到内存中。 byte[] buf = baos.toByteArray(); // 从内存流(字节数组流)中得到long数的byte数组。 // 创建数据报。 DatagramPacket dp = new DatagramPacket(buf, buf.length, new InetSocketAddress("127.0.0.1", 5678)); DatagramSocket ds = new DatagramSocket(9999); // 占用9999端口。 ds.send(dp); // 发送数据报。 ds.close(); // 关闭通信socket。 } }