`
linliangyi2007
  • 浏览: 1012587 次
  • 性别: Icon_minigender_1
  • 来自: 福州
社区版块
存档分类
最新评论

Graceful Java Programming 优雅Java编程 之Socket Client

阅读更多
   老久没有动手写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方法实现更优雅。

一点小经验,和大家分享

分享到:
评论
47 楼 tianyidan 2010-01-29  
多谢分享,看了这么多的回复,收益颇多。
46 楼 xuyan2680 2009-09-15  
  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);

也遇到了类似的问题。这样确实可以解决
45 楼 linliangyi2007 2009-02-18  
没天理啊,好心找雷劈,写个帖子还被评新手贴,倒扣10分。无语
44 楼 linliangyi2007 2009-02-17  
wandou 写道
socket的tcp协议,是按照流来抽象的。所谓流,就是与发送的时间和次数无关的,只和发送字节序无关。
比如发送"ABC",是一次发出"ABC",还是分三次发出"A","B","C",即使这A,B,C都处在三个报文中,对于接收端来讲都是没有区别的。
如果分三次发出,对于接收端的read函数会怎么样?它一般会在读到A的时候,返回1次,读到B的时候,返回1次,读到C的时候返回1次。每次返回长度都是1个字节。所以,对这种情况,就应该读到所有的字节,才返回。
这些都是一些处理socket的基本常识。跟代码是否优雅无关。
楼主的代码,似乎竭力想把一个完整的报文一次发出,这个概念就是错误的。就算1个字节1个字节的发,服务端的程序也应该能处理,否则服务端的程序就是错误的。因为在网络不正常的时候,可能会采用较小的报文。比如,当前双方的通讯速度是5个字节/秒,那tcp协议栈就不会一下子发出一个1000字节的报文。


谢谢,第一次有人这么清晰理论的解释了“流”,受教了!一段非常简单的程序,能得到大家的关注,让某人我收益良多!

至于“优雅”更多是出于我对java的喜爱,我的有一系列的blog都用“优雅java编程”来起头的,哈哈,大家就不用在这上面较真了。
43 楼 wandou 2009-02-17  
socket的tcp协议,是按照流来抽象的。所谓流,就是与发送的时间和次数无关的,只和发送字节序无关。
比如发送"ABC",是一次发出"ABC",还是分三次发出"A","B","C",即使这A,B,C都处在三个报文中,对于接收端来讲都是没有区别的。
如果分三次发出,对于接收端的read函数会怎么样?它一般会在读到A的时候,返回1次,读到B的时候,返回1次,读到C的时候返回1次。每次返回长度都是1个字节。所以,对这种情况,就应该读到所有的字节,才返回。
这些都是一些处理socket的基本常识。跟代码是否优雅无关。
楼主的代码,似乎竭力想把一个完整的报文一次发出,这个概念就是错误的。就算1个字节1个字节的发,服务端的程序也应该能处理,否则服务端的程序就是错误的。因为在网络不正常的时候,可能会采用较小的报文。比如,当前双方的通讯速度是5个字节/秒,那tcp协议栈就不会一下子发出一个1000字节的报文。
42 楼 linliangyi2007 2009-02-17  
luanma 写道
难道不是么?

开始一堆的socket参数设置,其实不同的应用场景有不同的设置,这个跟优雅无关。

使用char读数据,charset也完全是跟c服务器要一致,如果优雅,应该使用一个通用的解决方案。其实这个问题还有c以0结尾的问题,都属于通讯协议的设计问题。似乎跟优雅也无关吧。

把这些东西扒掉,就剩下建立连接,最简单的读写数据,然后关闭连接。这不是hello world,还是啥?





原来是这样啊,看来兄弟的要求很高,无话可说了。

java原来就是以简单为优雅的,程序结构清晰,逻辑越简单约好。
我发贴要说的都在贴的最后说了,这个贴本来就不是给高手准备的,怠慢了,呵呵!
41 楼 bachmozart 2009-02-17  
成抬杠贴了
40 楼 luanma 2009-02-17  
难道不是么?

开始一堆的socket参数设置,其实不同的应用场景有不同的设置,这个跟优雅无关。

使用char读数据,charset也完全是跟c服务器要一致,如果优雅,应该使用一个通用的解决方案。其实这个问题还有c以0结尾的问题,都属于通讯协议的设计问题。似乎跟优雅也无关吧。

把这些东西扒掉,就剩下建立连接,最简单的读写数据,然后关闭连接。这不是hello world,还是啥?



39 楼 andot 2009-02-17  
luanma 写道
也就是一socket版的hello world,哪里优雅了?


非 hello world 的代码贴上十几屏,你愿意读吗?
38 楼 linliangyi2007 2009-02-17  
bachmozart 写道
我的理解:

socket传输本身是byte字节流,socket底层发送单位是segment,对端判断是否发送完毕的方法是:

1.对端关闭连接,这种情况很好判断,我只记得非阻塞IO
Java中 read返回 -1    ,  如果本次没有读到数据返回 0
C中    read返回  0    ,  如果本次没有读到数据得到一个EWOULDBLOCK 或EAGAIN错误,read返回 -1

2.通过协议判断,二进制协议可以参照CMPP,SGIP看看,文本协议可以参照Memcached的
该读几个字节结束在协议中都是规定好的

至于字符占多少字节什么的,跟Socket没有任何关系,传输层的东西和表示层的东西怎么能一起考虑呢


正解!!我正怀疑java中read出的-1是不是因为对方关闭了输出端链路造成的,你的说法印证了我的猜测(偷懒了,没有自己做试验,哈哈)

第二点中说的协议,也正是我上述说的content length的判断方式,跟字符编码无关!
跟楼上的英雄所见啊,哈哈!


这里再次重申,我的demo程序中只需要读取标准ascii码,才写成了read char的方式,这是在正确的场合恰当使用java
api的做法!!!



37 楼 linliangyi2007 2009-02-17  
djsl6071 写道
其实楼上有位仁兄回复得不错,楼主说他急了些,没看后面你的回复,但我倒觉得他是指出了你回复里的一些问题。

使用char传输,字节长度是不同的,但java的charset支持针对各种常见编码字符集转换来获得正确的String(当然,如果你并不是文本,那甚至不需要考虑这个),java中之所以倾向使用byte,是因为要支持多种字符集编码方式,增加了coder可能出错地处理了字符的可能性,而byte作为原始方式就能便于修改,并同不同的字符集编码方式快速转换对接。

http中有Content-length,但是你的传输只是基于tcp,并不一定是http,所以长度设置并不一定可用是吧。这是很没法子的,我不肯定基于\0的识别方式能够有效判断传输结束,除非加上一个关键条件,那就是C++服务器上发送的每个数值都是2bytes的,这样是你用char接收的原因吧,但不指定inputstream的charset方式,read到的char却不一定就是服务器发送过来的两两字节对应,因为charset编码会按特定方式解码byte字节数组,补码返回char,这个过程中\0可能作为实际字符的一部分而被忽略。


我同意你说的对java charset的顾虑,但我之所以会直接使用java的read方法,本身就有应用的默认条件的。我们的通讯中只会传输英文和数字,不出现非标准ascii的字符。在这样的前提下为什么不直接使用java默认的api呢,否则就是对java api的浪费了。

还有你对content-length的理解太片面了,你一下子就说到了http,实际上很多基于TCP的通讯协议有自己的报文格式,里面都有固定的长度描述字段的。如果你做过一些短信网关类似的通讯,就经常有这样的协议。

第三,有你说的\0字符因为charset编码被忽略也是不正确的,在实验中,我们打印出了这个标准的ascii字符,它可是ascii编码的第一个字符啊。可以确定的是,它没有因为编码而被忽略。
36 楼 bachmozart 2009-02-17  
我的理解:

socket传输本身是byte字节流,socket底层发送单位是segment,对端判断是否发送完毕的方法是:

1.对端关闭连接,这种情况很好判断,我只记得非阻塞IO
Java中 read返回 -1    ,  如果本次没有读到数据返回 0
C中    read返回  0    ,  如果本次没有读到数据得到一个EWOULDBLOCK 或EAGAIN错误,read返回 -1

2.通过协议判断,二进制协议可以参照CMPP,SGIP看看,文本协议可以参照Memcached的
该读几个字节结束在协议中都是规定好的

至于字符占多少字节什么的,跟Socket没有任何关系,传输层的东西和表示层的东西怎么能一起考虑呢
35 楼 djsl6071 2009-02-17  
其实楼上有位仁兄回复得不错,楼主说他急了些,没看后面你的回复,但我倒觉得他是指出了你回复里的一些问题。

使用char传输,字节长度是不同的,但java的charset支持针对各种常见编码字符集转换来获得正确的String(当然,如果你并不是文本,那甚至不需要考虑这个),java中之所以倾向使用byte,是因为要支持多种字符集编码方式,增加了coder可能出错地处理了字符的可能性,而byte作为原始方式就能便于修改,并同不同的字符集编码方式快速转换对接。

http中有Content-length,但是你的传输只是基于tcp,并不一定是http,所以长度设置并不一定可用是吧。这是很没法子的,我不肯定基于\0的识别方式能够有效判断传输结束,除非加上一个关键条件,那就是C++服务器上发送的每个数值都是2bytes的,这样是你用char接收的原因吧,但不指定inputstream的charset方式,read到的char却不一定就是服务器发送过来的两两字节对应,因为charset编码会按特定方式解码byte字节数组,补码返回char,这个过程中\0可能作为实际字符的一部分而被忽略。
34 楼 bachmozart 2009-02-16  
luanma 写道
也就是一socket版的hello world,哪里优雅了?


何必说这样的话呢,人家愿意分享心得本来就是好事,你如果有什么见解或经验,完全可以写出来呀,撂下这么一句话让人看着实在不舒服,分享有错吗?
33 楼 linliangyi2007 2009-02-16  
luanma 写道
也就是一socket版的hello world,哪里优雅了?


请问,socket的非hello world版,在java上要怎么写呢!?
32 楼 luanma 2009-02-16  
也就是一socket版的hello world,哪里优雅了?
31 楼 linliangyi2007 2009-02-13  
unsid 写道
linliangyi2007 写道
danielzhan 写道
我做过的一个项目正好相反,是Java的服务端和C++的客户端通讯.
对于Java的Server,大家有什么建议.


在网络通信方面,最终的处理跟语言的干系不大,还是看双方的通信协议是怎样的,一切以通讯协议的特点出发吧。


虽然是这样,但是不同语言还是有各自的最佳实践方式和管用技巧,这个又是不尽相同,不了解语言特性就会走弯路


恩,一个新思维,受教了。
大家都来谈谈啊!尤其是做C和C++的高手们,不要吝啬你们的知识啊
30 楼 unsid 2009-02-13  
linliangyi2007 写道
danielzhan 写道
我做过的一个项目正好相反,是Java的服务端和C++的客户端通讯.
对于Java的Server,大家有什么建议.


在网络通信方面,最终的处理跟语言的干系不大,还是看双方的通信协议是怎样的,一切以通讯协议的特点出发吧。


虽然是这样,但是不同语言还是有各自的最佳实践方式和管用技巧,这个又是不尽相同,不了解语言特性就会走弯路
29 楼 linliangyi2007 2009-02-12  
danielzhan 写道
我做过的一个项目正好相反,是Java的服务端和C++的客户端通讯.
对于Java的Server,大家有什么建议.


在网络通信方面,最终的处理跟语言的干系不大,还是看双方的通信协议是怎样的,一切以通讯协议的特点出发吧。
28 楼 danielzhan 2009-02-12  
我做过的一个项目正好相反,是Java的服务端和C++的客户端通讯.
对于Java的Server,大家有什么建议.

相关推荐

    Go-graceful优雅的重载http服务器零宕机时间兼容systemdsupervisor

    本文将深入探讨如何使用Go-graceful库来实现优雅的HTTP服务器重载,确保在更新服务时实现零宕机时间,并同时兼容systemd和supervisor这两种常见的系统管理工具。 `Go-graceful`是一个专门设计用于处理HTTP服务器的...

    graceful:写优雅服务的库

    控制优雅的应用程序 优雅的应用程序可以用信号来控制。 主进程支持以下信号 期限/INT 快速关机 放弃正常关机 合肥 重启工人 启动新的工作进程 优雅关闭旧的工作进程 USR2 升级可执行文件(启动新的主进程和工作进程...

    Laravel开发-graceful-cache

    `graceful-cache`是一个针对Laravel的缓存存储库的包装器,其目标是提供一种更加优雅、灵活的方式来管理你的应用缓存。在这个项目中,开发者可以更好地控制缓存的生命周期,提高应用的效率。 `graceful-cache`的...

    前端开源库-graceful-readlink

    优雅的前端开源库——`graceful-readlink`是一个针对`fs.readlink`进行优化的模块,主要用于解决Node.js在处理符号链接时可能出现的问题。在Node.js原生的`fs`模块中,`fs.readlink`函数用于读取符号链接(symbolic ...

    前端开源库-graceful-ncp

    `graceful-ncp`是一个专门为前端开发者设计的开源库,它实现了优雅的、异步的文件和目录的递归复制功能。这个库的核心目标是提供一种更稳定、更易用的方式来处理文件系统操作,特别是在处理大量文件或复杂目录结构时...

    Graceful Graph

    优雅图(Graceful Graph)是图论中的一个概念,它源于数学和计算机科学,特别是网络流和图遍历的问题。这个概念是由美国数学家Edward G. Kolaitis在1970年代提出的,目的是研究图的标号特性。在英特尔线程挑战赛3月...

    前端开源库-graceful-kill

    本文将深入探讨“graceful-kill”的概念、作用以及它如何改善前端应用程序的性能。 1. 优雅地杀死进程 在计算机系统中,进程的正常退出通常涉及清理资源、保存状态和关闭连接等操作。"优雅地杀死"一个进程意味着...

    java程序发布成window后服台

    Java程序在Windows环境下发布为后台服务是常见的需求,这使得应用程序能够在系统启动时自动运行,无需用户交互。本文将详细讲解如何将一个Java程序打包并发布为Windows后台服务,同时提供一个具体的示例。 首先,...

    graceful-fs-fs模块的一个替代拥有各种改进

    优雅的fs(graceful-fs)是Node.js生态系统中一个重要的模块,它作为原生fs模块的一个增强版本,提供了一系列的改进和优化,旨在提高文件系统的操作性能和稳定性。在Node.js开发过程中,文件系统操作是不可或缺的一...

    Laravel开发-graceful-laravel-workers

    3. **优雅关闭(Graceful Shutdown)**:在“graceful-laravel-workers”中,主要目标是实现优雅关闭,即在接收到停止命令时,工作进程会等待当前处理的任务完成后再退出,确保数据的一致性和完整性。 4. **...

    express-graceful-exit, 为了得到零停机,优雅地退出 Express.zip

    express-graceful-exit, 为了得到零停机,优雅地退出 Express express-graceful-exit具有零停机时间的组件为 node.js 使用 Express 部署。 它是为 3. X, 开发的,因此它可能需要与 Express 2.x 兼容。这个模块最初是...

    socket常见错误代码解析.pdf

    Socket Error 10094是指优雅关闭正在进行中,这种错误通常是由于Socket关闭正在进行中所致。解决方法是等待Socket关闭完成。 Socket Error 11001 - Host not found Socket Error 11001是指主机不存在,这种错误...

    Functional Programming in C++

    Mastering the functional style of programming can help you tackle the demands of modern apps and will lead to simpler expression of complex program logic, graceful error handling, and elegant ...

    Scalable IO in Java.pdf

    在讨论可伸缩性目标时,文档强调了在增加负载时的优雅退化(Graceful degradation),即在用户数量增加时,系统能够合理地使用增加的资源(CPU、内存、磁盘、带宽)来提升性能。同时,系统应当满足可用性与性能目标...

    RFC8538 Notification Message Support for BGP Graceful Restart

    **BGP Graceful Restart**(优雅重启)是一种使边界网关协议(BGP)能够在路由器重启时保持部分连接状态的方法,从而减少对网络的影响。在**RFC4724**中定义了这一机制,但它限制了BGP优雅重启的应用范围,即不包括...

    javaAJAX无刷技术

    在实际开发中,为了兼容各种浏览器和提升用户体验,开发者通常会结合使用AJAX和其他技术,例如使用渐进增强(Progressive Enhancement)或优雅降级(Graceful Degradation)策略,确保即使在JavaScript禁用或不支持...

    前端开源库-graceful-fs-extra

    `graceful-fs-extra`是一个针对Node.js环境的`fs`模块扩展库,它在原生`fs`模块的基础上增加了一些实用功能,并提供了更优雅的错误处理机制,使得文件操作更加高效和稳定。本文将深入探讨`graceful-fs-extra`库的...

    Scalable IO in Java -Doug Lea

    在Scalable IO in Java中,Doug Lea首先介绍了网络服务的可扩展性目标,包括在增加负载(即更多客户端)的情况下优雅地降级(graceful degradation),以及在资源(如CPU、内存、磁盘和带宽)增加时持续改进性能。...

    BGP Graceful Restart

    【BGP Graceful Restart】是BGP(Border Gateway Protocol)的一种特性,旨在确保在路由器或协议处理组件(如Route Processor,RP)重启时,路由信息的连续性和稳定性,避免网络中断。这一特性允许BGP Speaker...

    go-graceful-restart-example, 具有优雅重启功能的服务器示例.zip

    go-graceful-restart-example, 具有优雅重启功能的服务器示例 服务器使用 Go graceful重新启动安装并运行服务器$ go get github.com/Scalingo/go-graceful-restart-example$ go-graceful-restart-examp

Global site tag (gtag.js) - Google Analytics