论坛首页 入门技术论坛

Graceful Java Programming 优雅Java编程 之Socket Client

浏览 24474 次
该帖已经被评为新手帖
作者 正文
   发表时间:2009-02-06   最后修改:2009-03-16
   老久没有动手写Socket程序了,今天应同事的要求上了一段程序。

这是一段很简单与C++编写的服务端通讯的java客户端,咋一看上去,没有任何问题。

貌似没有问题的程序

	public static String sendSynMsg(String ipAddr, byte[] datas) throws Exception{
		//解析服务器地址和端口号
		int dotPos = ipAddr.indexOf(':');
		String ip = ipAddr.substring(0, dotPos).trim();
		int port = Integer.parseInt(ipAddr.substring(dotPos+1).trim());
		InetSocketAddress endpoint = new InetSocketAddress(ip , port);
		
		Socket socket = null;
		OutputStream out = null;
		InputStream in = null;
		try	{		
			socket = new Socket();
			//设置发送逗留时间2秒
			socket.setSoLinger(true, 2); 
			//设置InputStream上调用 read()阻塞超时时间2秒
			socket.setSoTimeout(2000);
			//设置socket发包缓冲为32k;
			socket.setSendBufferSize(32*1024);
			//设置socket底层接收缓冲为32k
			socket.setReceiveBufferSize(32*1024);
			//关闭Nagle算法.立即发包
			socket.setTcpNoDelay(true);
			//连接服务器
			socket.connect(endpoint);
			//获取输出输入流
			out = socket.getOutputStream();
			in = socket.getInputStream();
			//输出请求			
			out.write(datas);
			out.flush();
			//接收应答
			BufferedReader br = new BufferedReader( new InputStreamReader(in) , 4096);
			StringWriter received = new StringWriter(4096);
			char[] charBuf = new char[4096];
			int size = 0;
			while ((size = br.read(charBuf)) > 0){
				received.write(charBuf, 0, size);
			}
			return received.toString();
			
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}
			if (in != null) {
				try {
					in.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}		
			if (socket != null) {
				try {
					socket.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}
		}				
	}


但实际的调试中,总是报Read TimeOut异常!!排查原因后发现,数据是接收到了,只是size = br.read(charBuf)不会返回-1(java doc中的说明是读取到结束时size会返回-1)。
对于编写服务器端程序的C++程序员而言,他们通常会在通讯结束的时候,在数据的尾部加上一个\0的结束字符。因此,我们针对此做了修正

出问题的程序段
    while ((size = br.read(charBuf)) > 0){
        received.write(charBuf, 0, size);
    }


修改后的正确写法
  char lastChar = 0;
  do {
      size = br.read(charBuf , 0 , 4096);
      lastChar = charBuf[size-1];
      if(lastChar == 0){
          //去除尾部的\0字符
          received.write(charBuf, 0, size - 1);
      }
  }while(lastChar != 0);


最终的完整程序段:
	private static String sendSynMsg(String ipAddr, byte[] datas) throws Exception{
		//解析服务器地址和端口号
		int dotPos = ipAddr.indexOf(':');
		String ip = ipAddr.substring(0, dotPos).trim();
		int port = Integer.parseInt(ipAddr.substring(dotPos+1).trim());
		InetSocketAddress endpoint = new InetSocketAddress(ip , port);
		
		Socket socket = null;
		OutputStream out = null;
		InputStream in = null;
		try	{		
			socket = new Socket();
			//设置发送逗留时间2秒
			socket.setSoLinger(true, 2); 
			//设置InputStream上调用 read()阻塞超时时间2秒
			socket.setSoTimeout(2000);
			//设置socket发包缓冲为32k;
			socket.setSendBufferSize(32*1024);
			//设置socket底层接收缓冲为32k
			socket.setReceiveBufferSize(32*1024);
			//关闭Nagle算法.立即发包
			socket.setTcpNoDelay(true);
			//连接服务器
			socket.connect(endpoint);
			//获取输出输入流
			out = socket.getOutputStream();
			in = socket.getInputStream();
			//输出请求			
			out.write(datas);
			out.flush();
			//接收应答
			BufferedReader br = new BufferedReader( new InputStreamReader(in) , 4096);
			StringWriter received = new StringWriter(4096);
			char[] charBuf = new char[4096];
			int size = 0;
			char lastChar = 0;
			do {
				size = br.read(charBuf , 0 , 4096);
				lastChar = charBuf[size-1];
				if(lastChar == 0){
					received.write(charBuf, 0, size - 1);
				}
				//System.out.println(received.toString());
			}while(lastChar != 0);
			
			return received.toString();
			
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}
			if (in != null) {
				try {
					in.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}		
			if (socket != null) {
				try {
					socket.close();
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}
		}				
	}



程序中的关键点:
1. 不直接使用new Socket(String ip , int port)的构造函数,而是设置了socket的环境参数后再连接

2.设置发送逗留时间 socket.setSoLinger(true, 2); 这个参数是socket发送数据时的超时,如果对方在固定时间内不接受,则关闭socket。与socket.setSoTimeout(2000)不同,这个是设置InputStream上调用 read()阻塞超时时间。

3.socket.setTcpNoDelay(true);关闭Nagle算法。这使得在调用out.flush();时总能第一时间的发送数据包(这个适用于你的数据包是完整的一次性发送的前提)。

4.根据应用协议的实际大小,优化你的接收和发送缓冲,这两个参数可以有效提高网络通信的效率。

5.使用char的数组配合StringWriter作为接收数据的写入,这个比使用readLine方法实现更优雅。

一点小经验,和大家分享

   发表时间:2009-02-06  
恩我喜欢这个话题,问几个问题:
(1) 如果new Socket(String ip , int port),那么之后的socket.setSoLinger(true, 2); 是不是就不管用了???所以必须先socket.setSoLinger(true, 2)再connect???

(2) 我最感兴趣的是br.read(charBuf)这句阻塞到什么时候..按照你写的socket.setSoTimeout(2000);
  1/ 是不是br.read(charBuf)这句最多只等2秒?有可能在服务器端2s还没运算出数据..那样的话就不合理了..
  2/ 如果超时那么br.read(charBuf)是否返回-1跳出循环?

(3) 之前和别人讨论过关于网络通讯如何判断字节流是否接收完毕的话题,有人说"-1"是判断从文件读取的流,不能用来判断网络流,还有有人用in.avaliable()来判断该读取多少,你认为是否合理?我用in.avaliable()判断过,结果是不对的...
0 请登录后投票
   发表时间:2009-02-06  
另外关于IO,网上大多数帖子都是讲java各种io操作,各种包装类方法的,找不到对原理进行深入谈论的,我希望能和搂主多交流.
我有时候也下点源码看,我发现稍微有点规模的网络通讯程序,都没用java默认io,都有自己一套io包,比如下载hassian代码中用的都是HassianOutputStream之类的自定义IO,我很好奇它们把什么风装进去了,也许是hassian转有通讯机制,我想以后写网络通讯程序应该多学那些开源东西里的方法,多学IO原理,争取以后能写程序定义自己的IO库..
0 请登录后投票
   发表时间:2009-02-06  
1、setSoLinger的作用完全说错了,这个选项主要是设置socket关闭后的行为,具体请google或者参考unp
2、禁掉nagle算法也不能说总能第一时间发出数据包,只是底层不再帮你组合小的数据包到MSS大小左右再发送,如果你的数据没有小于40个字节,通常不建议关闭这个选项。

0 请登录后投票
   发表时间:2009-02-06   最后修改:2009-02-06
dennis_zane 写道
1、setSoLinger的作用完全说错了,这个选项主要是设置socket关闭后的行为,具体请google或者参考unp
2、禁掉nagle算法也不能说总能第一时间发出数据包,只是底层不再帮你组合小的数据包到MSS大小左右再发送,如果你的数据没有小于40个字节,通常不建议关闭这个选项。


关于楼上提到的第一点,我是参考Stevens 的http://mindprod.com/jgloss/socket.html#DISCONNECT 以及他的译稿http://frenchmay.iteye.com/blog/254503。
也许我说的不够清楚,实际上就是指socket的发送等待,在你调用的flush后,又close了socket,这个时候,在socket的底层缓冲可能还有数据尚未发出,这就要求socket等待数据全部发出后关闭。setSoLinger就是设置这个超时的。

关于第二点,楼上说的没有错,关于nagle的算法,我的说明也不够详细,但从效果上看,不关闭nagle,flush常会等待包到足够大后才真的发出,关闭了,则基本上就是立即发送了。


欢迎更多牛人指正!
0 请登录后投票
   发表时间:2009-02-06  
unsid 写道
恩我喜欢这个话题,问几个问题:
(1) 如果new Socket(String ip , int port),那么之后的socket.setSoLinger(true, 2); 是不是就不管用了???所以必须先socket.setSoLinger(true, 2)再connect???

(2) 我最感兴趣的是br.read(charBuf)这句阻塞到什么时候..按照你写的socket.setSoTimeout(2000);
  1/ 是不是br.read(charBuf)这句最多只等2秒?有可能在服务器端2s还没运算出数据..那样的话就不合理了..
  2/ 如果超时那么br.read(charBuf)是否返回-1跳出循环?

(3) 之前和别人讨论过关于网络通讯如何判断字节流是否接收完毕的话题,有人说"-1"是判断从文件读取的流,不能用来判断网络流,还有有人用in.avaliable()来判断该读取多少,你认为是否合理?我用in.avaliable()判断过,结果是不对的...


关于(1)点,不是先new就不起作用,但后set,我怀疑(仅仅是怀疑),至少要增加后续的网络通信。还请了解socket底层的牛人也来详细说明一下。但后connect,从程序上至少能保证设置一定有效,而且是在connect后立即生效吧!

关于(2),这个timeout参数的作用你说的没有错。当然,这是根据服务器的业务特点设定的,我们的业务完成只有不到50ms,2m钟的超时基本可以任务服务失败了,而前台的用户是希望尽快的高知失败,而不是等上10秒在告诉他任务失败。
其次,超时的时候,read方法不会有-1返回,而是抛出Read TimeOut异常。
0 请登录后投票
   发表时间:2009-02-07  
客户端主动连接发送第一个syn分节可能会携带你设置的TCP选项,主要是MSS和窗口规模通告,因此如果你要调整socket的缓冲区肯定要在发送第一个syn分节前(也就是connect前)设置了。而SO_LINGER选项是设置本地socket的, connect之前还是之后没有影响。
0 请登录后投票
   发表时间:2009-02-07  
dennis_zane 写道
客户端主动连接发送第一个syn分节可能会携带你设置的TCP选项,主要是MSS和窗口规模通告,因此如果你要调整socket的缓冲区肯定要在发送第一个syn分节前(也就是connect前)设置了。而SO_LINGER选项是设置本地socket的, connect之前还是之后没有影响。


谢谢楼上大拿。看来一定是做C/C++通讯的高手。对于我们这些学java的,一直为不能接触通讯底层而无奈。。。。
0 请登录后投票
   发表时间:2009-02-08  
这个主题好,虽然现在不用自己控制底层通讯细节了,但是不了解就会导致不能正确使用socket的api,尤其是socket通讯这样的程序即难以测试,又很难保证某一时刻不出意想不到的问题。。。。
0 请登录后投票
   发表时间:2009-02-08  
unsid 写道
这个主题好,虽然现在不用自己控制底层通讯细节了,但是不了解就会导致不能正确使用socket的api,尤其是socket通讯这样的程序即难以测试,又很难保证某一时刻不出意想不到的问题。。。。


百分之一千的赞同!!期待socket高人讲解,或者提供相关的资料!!
0 请登录后投票
论坛首页 入门技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics