一、JAVA网络编程概述
网络应用程序,就是在已实现网络互联的不同计算机上运行的应用程序,这些程序之间可以相互交换数据。JAVA是优秀的网络编程语言,Java网络编程的类库位于java.net包中。Java支持TCP/UDP及其上层的网络编程,对TCP/UDP以下层,如IP包的捕获,侦听,数据链路层的帧的捕获,需要借助第三方的java包,如UNIX/Linux下著名的libpcap包的Java版本jpcap包。
在网络编程中,服务器与客户程序只需关心发送什么样的数据给对方,而不必考虑如何把这些数据传输给对方,传输数据的任务由计算机网络完成。两个进程顺利通信的前提条件是它们所在的主机都连接到了计算机网络上。网络协议是网络中主机之间通信的语言。不同网络之间的互联靠网络上的标准语言——TCP/IP协议。
二、TCP网络编程
1.OSI参考模型
OSI参考模型把网络分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。不同主机之间的相同层称为对等层。
物理层:为上一层提供物理连接,以及规定通信节点之间的机械和电气特性。数据作为原始的比特(bit)流传输。
数据链路层:数据链路层负责在两个相邻节点间的线路上,无差错地传送以帧为单位的数据。典型设备是交换机。
网络层:选择合适的网间路由和交换节点,确保数据及时传送到目标主机。典型设备是路由器。
传输层:根据通信子网的特性最佳地利用网络资源,为两个端系统的会话层提供建立、维护和取消传输连接的功能,以可靠方式或不可靠方式传输数据。信息的传送单位是报文。
会话层:管理进程间的会话过程,即负责建立、管理、终止进程间的会话。数据传送单位是报文。
表示层:对上层数据进行转换,以保证一个主机的应用层的数据可以被另一个主机的应用层理解。数据传送单位是报文。
应用层:确定进程间通信的实际用途,以满足用户实际需求。
发送方每一层会给上一层传递来的数据加上一个信息头。接收方每一层会把信息头去掉。对等层之间互相通信需要遵守一定的规则,如通信的内容和通信的方式,这种规则称为网络协议。TCP/IP参考模型分为4个层次:应用层、传输层、网络互联层和主机-网络层。
2.TCP/IP参考模型
TCP/IP参考模型中有两个非常重要的协议:TCP协议:面向连接的、可靠的协议;UDP协议:不可靠的、无连接协议。IP协议:IP地址(IP网址+IP主机地址),网络掩码与IP地址进行二进制与操作,得到的结果就是IP网址。DNS协议采用DNS服务器来提供把域名转换为IP地址。TCP采用端口区分进程。TCP连接的两个端点用端口来标识。每个进程有了唯一的地址,TCP就能保证把数据顺利送达特定的进程。端口号的范围为0到65535。0-1023操作系统保留。服务端端口号需要指定,客户端端口号一般系统动态分配。TCP和UDP都用端口来标识进程,取值范围各自独立,允许存在取值相同的TCP端口和UDP端口。客户/服务器模式,一个服务器进程会同时为多个客户进程服务。
以下是IP地址分类:
传输层向应用层提供了套接字Socket接口,Socket封装了下层数据传输细节,应用层程序通过Socket来建立与远程主机的连接,以及进行数据传输。Java中有3种套接字类:Socket、ServerSocket、DatagramSocket。Socket和ServerSocket建立在TCP协议上,DatagramSocket建立在UDP协议上。
(一)Socket用法详解
客户端/服务器通信模式中,客户端需要主动创建与服务器连接的Socket,服务端收到客户端的连接请求,也会创建与客户连接的Socket。 等待建立连接的超时时间,默认情况下会一直等待下去,通过不带参数的Socket构造方法可以设定等待时间,单位毫秒,为0表示永远不会超时。 设定服务器地址,InetAddress 设定客户端地址,Socket对象既包含服务器IP和端口也包括客户端地址和端口,对于拥有多个IP地址的客户端可指定使用哪个IP。
1.IP地址表示类InetAddress
Internet上的主机有两种表示地址的方式:域名和IP地址。有时候需要通过域名来查找它对应的IP地址,有时候又需要通过IP地址来查找主机名。这时候可以利用java.net包中的InetAddress类来完成任务。
InetAddress类是IP地址封装类,同时它也是一个比较奇怪的类——没有公共的构造方法,只能利用该类的一些静态方法来获取对象实例,然后再通过这些对象实例来对IP地址或主机名进行处理。该类常用的一些方法有如下。
pulic static InetAddress getByName(String hostname):根据给定的主机名创建一个InetAddress对象,可用来查找该主机的IP地址。
public static InetAddress getByAddress(byte[] addr):根据给定的IP地址创建一个InetAddress对象,可用来查找该IP对应的主机名。
public String getHostAddress():获取IP地址。
public String getHostName():获取主机名。
练习示例:返回从控制台输入的域名相对应的IP地址,若没有给出域名,则返回本地主机的IP地址。
代码如下:
1 package com.OpenWealth.ZSY; 2 3 import java.io.*; 4 import java.net.InetAddress; 5 6 public class InetAddressDemo { 7 8 public static void main(String[] args ){ 9 10 //从键盘读取数据的标准写法 11 BufferedReader inBuff=new BufferedReader(new InputStreamReader(System.in)); 12 InetAddress[] ips=null; 13 try { 14 String domainName=inBuff.readLine(); 15 ips=InetAddress.getAllByName(domainName);//一个域名对应的主机可能有多个ip地址 16 System.out.println("域名:"+domainName+"对应的主机ip地址有:"); 17 for(InetAddress ip : ips){ 18 System.out.println("ip::"+ip.toString()); 19 } 20 } catch (IOException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } 24 } 25 }
运行结果:
2.Socket通信过程
客户连接服务端的异常:UnknownHostException(无法识别主机名或IP地址),ConnectionException(服务端没有监听指定端口或者拒绝连接),SocketTimeoutException(连接超时),BindException(无法把Socket对象与客户端IP或端口绑定)。
getInputStream和getOutputStream:如果Socket还没有连接或者已经关闭,或者已经调用shutdownInput(shutdownOutput)方法,则关闭输入输出流。
当通信结束,应及时通过close方法关闭Socket。强烈建议放在finally代码中确保总是执行。
Socket状态:
isClose(如果已连接到远程主机,且还未关闭,返回true);
isConnected(如果曾经连接到远程主机,返回true);
isBound(如果已经与一个本地端口绑定,则返回true)半关闭Socket。
结束通信的几种方式:
(1)字符流:约定特殊字符;
(2)约定数据长度;
(3)发送方发完数据后,关闭Socket,这时接收端调用输入流的read方法返回 值为-1; (4)关闭输入或输出流,最终还需要调用Socket的close方法关闭Socket连接isInputShutdown和isOutputShutdown用于检测输入输出流状态。
通信双方的终止方式:
(1)自然结束;
(2)提前终止服务器端:服务器端突然结束,客户端会仍然会发送完全部数据后自然结束,这是因为服务器端底层Socket并没有立即释放,操作系统探测到还有发送给该端口的数据,会继续占用一断时间;
(3)突然终止客户端:抛出Connection reset异常;
(4)关闭或半关闭:服务器端抛出Connection reset异常Socket选项:TCP_NODELAY:是否立即发送数据,Negale算法,发送大批量数据,并且接收端及时响应的情况下很有用,如果是发送少量数据并且接收端不及时响应则会严重降低性能,采用该算法将把发送数据缓冲起来到一定大小后一次性发出,等待接收端响应后再发送下一批数据。 默认false采用该算法。
SO_RESUSEADDR:调用Socket的close方法关闭连接后,不会立刻端口,这时其他进程绑定到该端口将失败,设置SO_RESUSEADDR为true将使得其他进程可以重用这个端口。必须在绑定到端口之前调用。一般是服务器端存在该问题。新进程不会收到之前的数据。 SO_TIMEOUT:接收数据的等待超时时间,单位毫秒,默认0表示永远等待。要在接收数据之前设置。
SO_LINGER:Socket连接是否立即关闭SO_SNFBUF:输出数据缓冲区大小,大批量数据建议大点,频繁传送小数据建议小点SO_RCVBUF:输入数据缓冲区大小。 SO_KEEPALIVE:为true时底层实现会监视连接有效性,客户端使用 OOBINLINE:为true时表示支持发送一个字节的紧急数据,一般不用。
服务类型:
IP规定了4中服务类型:1、低成本:发送成本低;2、高可靠性:保证鲍数据可靠的发送到目的地;3、高吞吐量:一次接受发送大量数据;4、最小延迟:传输数据快,最快发送到目的地设置服务类型:setTrafficClass(int trafficClass);底成本:0x02 高可靠:0x04 最高吞吐量:0x08最小延迟:0x10连接时间、延迟、带宽的相对重要性:setPerformancePreference。
(二)ServerSocket用法详解
服务器端创建ServerSocket监听特定端口,ServerSocket负责接收客户连接请求。 ServerSocket三个参数:port,backlog,bindAddr。
绑定端口port:绑定失败将抛出BindException,可能由于端口已被占用或端口不能被该用户使用(例如1~1023可能只能用root用户绑定)所致,参数0表示操作系统分配,称为匿名端口。
客户连接请求队列的长度:客户端连接请求由操作系统的先进先出队列管理,ServerSocket通过accept方法取出该队列的请求,backlog用于改变请求队列长度,但是如果该值大于操作系统限制或小于等于0则依然使用操作系统的值。
设定绑定的IP地址:bindAddr参数,多IP时可以指定绑定到哪个IP 不带参数的构造器:创建完ServerSocket对象后需要调用bind方法,bind之前可以设置一些参数,有些参数必须在bind之前调用。accept方法会阻塞,客户端连接上来后才返回。 ServerSocket不用close,程序结束时会释放,也可显示调用。ServerSocket的isClosed,isBound与Socket类似。
获取服务器绑定IP和端口:getInetAddress和getLocalPort FTP使用匿名端口 ServerSocket选项:
- SO_TIMEOUT:accept等待客户端连接超时的时间,0表示永远不超时,一直等 待客户端连接。
- SO_REUSEADDR:和Socket的类似
- SO_RCVBUF:接收数据的缓冲区的大小,与操作系统有关。连接时间、延迟和带宽的相对重要性:与Socket类似 多线程服务器(重点)前面的例子中的服务器都无法同时响应多个客户连接请求。
器与客户端之间的通信:
练习示例:客户端程序向服务端程序同时上传多张图片,服务端程序可以响应客户端并发访问,实现图片的多线程上传功能。
代码如下:
1 package com.OpenWealth.ZSY; 2 3 import java.io.File; 4 import java.io.FileNotFoundException; 5 import java.io.IOException; 6 import java.net.*; 7 import java.util.ArrayList; 8 import java.util.List; 9 import java.io.*; 10 public class TCPClientSocket { 11 12 13 public static void main(String args[]) throws Exception{ 14 15 BufferedReader buff=new BufferedReader(new InputStreamReader(System.in)); 16 17 //同时上传多张图片 18 while(true){ 19 String imgUrl=buff.readLine(); 20 new Thread(new MyUpLoadPicClient(imgUrl,"localhost",8866)).start(); 21 } 22 23 } 24 25 26 /** 27 * 实现了同时上传多张图片的客户端;可以上传多种格式的图片 28 * @author 支胜勇 29 * 30 */ 31 public static class MyUpLoadPicClient implements Runnable{ 32 33 private File imgFile; 34 35 private String ip; 36 37 private int port; 38 39 private Socket soc; 40 41 private List<String> extensList=null; 42 43 private OutputStream out; 44 private FileInputStream fis; 45 46 47 /*** 48 * 初始化构造方法 49 * @param imgUrl 50 * @param ip:服务器IP地址 51 * @param port:服务器端口 52 * @throws Exception 53 */ 54 public MyUpLoadPicClient(String imgUrl,String ip,int port) throws Exception{ 55 56 this.imgFile=new File(imgUrl); 57 this.ip=ip; 58 this.port=port; 59 60 extensList=new ArrayList<String>(); 61 extensList.add(".jpg"); 62 extensList.add(".png"); 63 extensList.add(".gif"); 64 extensList.add(".jpeg"); 65 extensList.add(".bmp"); 66 67 } 68 69 70 public boolean isImg(){ 71 72 if(!imgFile.exists()){//||!imgFile.isFile() 73 System.out.println("图片不存在,请重新上传!"); 74 return false; 75 } 76 77 if(!imgFile.isFile()){//||!imgFile.isFile() 78 System.out.println("不是图片文件,请重新上传!"); 79 return false; 80 } 81 82 String extenName=imgFile.getName().substring(imgFile.getName().lastIndexOf(".")).toLowerCase(); 83 if(!extensList.contains(extenName)){ 84 85 System.out.println("请上传jpg,png,gif,bmp,jpeg格式的图片"); 86 return false; 87 } 88 89 if(imgFile.length()>1024*1024*5){ 90 System.out.println("图片过大,请上传5m以内的图片!"); 91 return false; 92 } 93 94 try { 95 96 fis=new FileInputStream(imgFile); 97 98 } catch (FileNotFoundException e) { 99 // TODO Auto-generated catch block 100 e.printStackTrace(); 101 } 102 return true; 103 } 104 105 106 /*** 107 * 执行向服务器上传图片的代码 108 */ 109 @Override 110 public void run() { 111 // TODO Auto-generated method stub 112 if(!isImg()){ 113 return; 114 }else{ 115 try { 116 this.soc=new Socket(this.ip,this.port); 117 out=soc.getOutputStream(); 118 byte[] buff=new byte[1024]; 119 int len=0; 120 while( (len=fis.read(buff)) != -1 ){ 121 122 out.write(buff,0,len); 123 } 124 soc.shutdownOutput();//告诉服务器上传结束 125 126 InputStream in=soc.getInputStream(); 127 128 byte[] bufIn=new byte[1024]; 129 130 int num=in.read(bufIn); 131 132 System.out.println(new String(bufIn,0,num)); 133 134 fis.close(); 135 out.close(); 136 soc.close(); 137 } catch (IOException e) { 138 // TODO Auto-generated catch block 139 e.printStackTrace(); 140 } 141 142 } 143 } 144 } 145 }
1 package com.OpenWealth.ZSY; 2 3 import java.io.*; 4 import java.net.*; 5 6 public class TCPServer{ 7 8 9 @SuppressWarnings("resource") 10 public static void main(String[] args) throws Exception{ 11 12 ServerSocket ss=null; 13 ss=new ServerSocket(8866); 14 while(true){ 15 16 try { 17 18 Socket soc=ss.accept(); 19 20 new MyPicServerThread(soc).start();//开启服务端接受图片的线程 21 22 } catch (IOException e) { 23 // TODO Auto-generated catch block 24 e.printStackTrace(); 25 } 26 } 27 } 28 29 30 31 /*** 32 * 实现了多用户并发上传图片的服务端 33 * @author 支胜勇 34 * 35 */ 36 public static class MyPicServerThread extends Thread{ 37 38 39 private Socket soc=null; 40 41 public MyPicServerThread(Socket _soc){ 42 this.soc=_soc; 43 } 44 45 @Override 46 public void run() { 47 // TODO Auto-generated method stub 48 String clientIp=soc.getInetAddress().getHostAddress(); 49 try { 50 51 System.out.println(clientIp+"已连接!"); 52 53 InputStream in=soc.getInputStream(); 54 55 FileOutputStream fos=new FileOutputStream("E:\TcpTest.png"); 56 57 byte[] buff=new byte[1024]; 58 59 int len=0; 60 61 while((len=in.read(buff))!=-1){ 62 63 fos.write(buff,0,len); 64 } 65 66 OutputStream out=soc.getOutputStream(); 67 68 out.write("图片上传成功!".getBytes()); 69 70 fos.close(); 71 72 soc.close(); 73 74 } catch (IOException e) { 75 // TODO Auto-generated catch block 76 throw new RuntimeException("客户端"+clientIp+"上传失败"); 77 } 78 79 } 80 } 81 }
运行结果: