java网络编程02-Socket用法详解

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

1、构造Socket

Socket的构造方法有以下几种重载形式:
(1)Socket()
(2)Socket(InetAddress address, int port)throws UnknownHostException,IOException
(3)Socket(InetAddress address, int port, InetAddress localAddr, int localPort)throws IOException
(4)Socket(String host, int port) throws UnknownHostException,IOException
(5)Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException

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

各构造方法的用法如下

1.1、设定等待建立连接的超时时间(Socket())

Socket构造方法请求连接时,由于各种大原因,可能会处于长时间的等待状态,该如何限制等待时间,此时需要Socket不带参数的构造方法:

Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr,60000);

以上代码用于连接到本地主机上的8000端口的服务器程序,等待连接的最长时间为1分钟。超过1分钟没有连接成功,刚会报SocketTimeoutException。

1.2、设定服务器地址

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

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

InetAddress类表示服务器的IP地址,由于InetAddress没有public类型的构造方法,所以只能通过其自身提供的一系列的静态方法来构造自身的实例,例如

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

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

Socket(InetAddress address,int port, InetAddress localAddress, int localPort);
Socket(String host, int port, InetAddress localAddress, int localPort);	//参数localAddress和localPort用来设置客户端的ip地址和端口
这两个构造方法主要适用于以下情况:一个主机同时属于两个以上的网络,它就可能拥有两个以上的IP地址。例如,一个主机在Internet网络中的IP地址为“222.67.1.34”,在一个局域网中的IP地址为“112.3.6.56”。

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

当Socket的构造方法请求连接服务器时,可能会抛出以下异常:
* UnknownHostException:如果无法识别主机的名字或IP地址,就会抛出这种异常。
* ConnectException:如果没有服务器进程监听指定的端口,或者服务器进程拒绝连接,就会抛出这种异常。
* SocketTimeoutException:如果等待连接超时,就会抛出这种异常。
* BindException:如果无法把Socket对象与指定的本地IP地址或端口绑定,就会抛出这种异常。

以上4种异常都是IOException的直接或间接子类。

2、获取Socket信息

在一个Socket对象中,同时包含了远程服务器的IP地址和端口信息,也包含了本地客户端的IP地址和端口信息。此外,Socket对象还可以获取输出流和输入流,分别用于向服务器发送请求信息的接收响应信息,以下方法用于获取Socke的有关信息:

* getInetAddress():获取远程服务器IP地址
* getPort():获得远程服务器的端口
* getLocalAddress():获得客户本地的IP地址
* getLocalPort():获得客户本地端口
* getInputStream():获得输入流
* getOutputStream():获得输出流

如下所示的例程中,HTTPClient类用于访问网页www.javathinker.org/index.jsp。该网页位于一个主机名为www.javathinker.org的远程服务器上,它监听服务器主机的80端口。在HTTPClient类中,先创建一个连接到HTTP服务器的Socket对象,然后发送HTTP协议的请求,并接收从HTTP服务器上发回的响应结果。

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class HTTPClient {
	String host = "www.javathinker.org";
	int port = 80;
	Socket socket;

	public void createSocket() throws Exception {
		socket = new Socket("www.javathinker.org", 80);
		boolean isok01 = socket.isClosed();
		boolean isok02 = socket.isConnected();
		boolean isok03 = socket.isBound();
		System.out.println(isok01);
		System.out.println(isok02);
		System.out.println(isok03);
	}

	public void communicate() throws Exception {
		StringBuffer sb = new StringBuffer("GET " + "/index.jsp"
				+ " HTTP/1.1\r\n");
		sb.append("Host: www.javathinker.org\r\n");
		sb.append("Accept: */*\r\n");
		sb.append("Accept-Language: zh-cn\r\n");
		sb.append("Accept-Encoding: gzip, deflate\r\n");
		sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
		sb.append("Connection: Keep-Alive\r\n\r\n");

		// 发出HTTP请求
		OutputStream socketOut = socket.getOutputStream();
		socketOut.write(sb.toString().getBytes());
		socket.shutdownOutput(); // 关闭输出流

		// 接收响应结果
		InputStream socketIn = socket.getInputStream();
		ByteArrayOutputStream buffer = new ByteArrayOutputStream(); // 缓冲区,用于存放接收到的字节
		byte[] buff = new byte[1024];
		int len = -1;
		while ((len = socketIn.read(buff)) != -1) {
			buffer.write(buff, 0, len);
		}
		System.out.println(new String(buffer.toByteArray())); // 把字节数组转换为字符串
		socket.close();
	}

	public static void main(String args[]) throws Exception {
		HTTPClient client = new HTTPClient();
		client.createSocket();
		client.communicate();
	}
}
3、关闭Socket

当客户端与服务器通信结束,应当及时关闭Socket,以释放Socket占用的包括端口在内的各种资源。Socket的close()方法负责关闭Socket,为了确保关闭的执行,建议将close()方法放在finally代码块中:

Socket socket = null;
try{
    socket = new Socket("www.javathinker.org",80);
    ……
}catch(IOException e){
    e.printStackTrace();
}finally{
    try{
        if(socket != null)
            socket.close();
    }
}
4、半关闭Socket

进程A与进程B通过Socket通信,假定里程A输出数据,里程B读入数据。进程A如何告诉进程B所有的数据都已经输出完毕呢?有以下几种处理办法:

4.1、当里程A与进程B交换的是字符流,并且都一行一行地读/写数据时,可以事先约定一个特殊的标志作为结束标志,当进程A向进程B发送了该结束标志,进程B读到这个结束标志后,就停止读取数据。

BufferedReader br =getReader(socket);
PrintWriter pw = getWriter(socket);

String msg = null;
while ((msg = br.readLine()) != null) {
    pw.println(echo(msg));
    if (msg.equals("bye")) //如果客户发送的消息为“bye”,就结束通信
        break;
}
4.2、进程A先发送一个消息,告诉进程B所发送的正文的长度,然后再发送正文。进程B先获知进程A所发送的正文的长度,接下来只要读取完该长度的字符或字节,就停止读取数据。
4.3、进程A发送完所有的数据后,关闭Socket。当里程B读入了进程A发送的的有数据后,再次执行输入流的read()方法时,该方法返回-1。如果执行BufferedReader的readline()方法时,返回null。

ByteArrayOutputStream buffer = new ByteArrayOutputStream;
byte[] buff = new byte[1024];
int len = -1;
while((len = socketIn.read(buff)) != -1){
    buffer.write(buff,0,len);
}
4.4、当socket的close()方法关闭Socket时,它的输入流和输出流也都被关闭。有的时候,可能仅仅希望关闭输入流或输出流之一,此时可采用Socket的半关闭方法:

* shutdownInput():关闭输入流

* shutdownOutput:关闭输出流

值得注意的是:先后调用的Socket的shutdownInput()和shutdownOutput()方法,关闭了输入流和输出流,但并不等于调用了Socket的close()方法,在通信结束后,仍然要调用Socket的close()方法,关闭Socket,释放Socket占用的资源。

5、设置Socket的选项

Socket有以下几个选项:

* TCP_NODEPLAY:表示立即发送数据

* SO_RESUSEADDR:表示是否允许重用Socket所绑定的本地地址

* SO_TIMEOUT:表示接收数据时的等待超时时间

* SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层Socket

* SO_SNFBUF:表示发送数据的缓冲区大小

* SO_RCVBUF:表示接收数据的缓冲区大小

* SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭

* OOBINLINE:表示是否支持发送一个字节的TCP紧急数据

5.1、TCP_NODEPLAY选项

设置该选项: 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 的输入流读数据时, 如果还没有数据, 就会一直等待,直到读取到数据或是连接断开,否则将一直等待。此时可通过Socket类的SO_TIMEOUT选项来设定接收数据的等待超时时间,单位为毫秒,它的默认值为0,表示会无限等待,永远不会超时。以下代码把接收数据的等待超时时间设为 3 分钟:
        if(socket.getSoTimeout() == 0)     socket.setSoTimeout(60000 * 3);

Socket 的 setSoTimeout() 方法必须在接收数据之前执行才有效。

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 参数以秒为单位, 而不是以毫秒为单位.

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 选项.

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, 就表示最高带宽最重要, 其次是最少连接时间, 最后是最小延迟.


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值