网络编程
1、网络参考模型
网络参考模型是控制网络的规范,所有的公司使用这个相同的规范来控制网络,就能互联了。最流行的参考模型有:OSI参考模型和TCP/IP参考模型。
两种网络参考模型的结构示意图如下:
网络中的数据通信是通过层与层之间封包、解封包的过程。
A机器的数据经过逐层封装,传到B机器,再经过逐层解封,B拿到了最终传输的数据。
OSI参考模型虽然设计精细,但过于麻烦,效率不高,因此才产生了简化版的TCP/IP参考模型。
2、网络通讯要素
网络通讯有三要素:IP地址,端口号,传输协议
一、IP地址,对应java中的InetAddress类。
1、ip地址是网络中设备的标识。不易记忆,可用主机名。
2、在没有连接互联网的情况,为了让访问本机方便,所以分配了一个默认的IP地址,也就是本地回环。本地回环地址:127.0.0.1 主机名:localhost。
3、通过ping 127.0.0.1可以测试网络是不是通,如果不通,可能是网卡出问题了。
InetAddress类无构造函数,可通过getLocalHost()方法获取InetAddress对象,此方法是静态的,返回本类对象。
InetAddress i = InetAddress.getLocalHost();
常用方法:
1)static InetAddress getByName(String host):获取指定主机的IP和主机名。(最好用ip地址去获取,主机名需要解析)
2)static InetAddress[] getAllByName(String host):在给定主机名的情况下,根据系统上配置的名称服务返回IP地址所组成的数组。返回对象不唯一时,用此方法。
3)String getHostAddress():返回IP地址字符串文本形式,以IP地址为主。
4)String getHostName():返回IP地址主机名。
二、端口号
端口号:用于标识进程(应用程序)的逻辑地址,不同进程的标识。有效端口:0~65535,其中0~1024系统使用或保留端口。
三、传输协议
传输协议就是通讯的规则。常见协议:UDP、TCP。
UDP:
1、将数据及源和目的封装成数据包中,不需要建立连接。
2、每个数据报的大小在限制在64k内。
3、因无连接,是不可靠协议。
4、不需要建立连接,速度快。
应用案例:QQ、FeiQ聊天、在线视频用的都是UDP传输协议。
TCP:
1、建立连接,形成传输数据的通道。
2、在连接中进行大数据量传输。
3、通过三次握手完成连接,是可靠协议
4、必须建立连接,效率会稍低。
应用案例:FTP,File Transfer Protocol(文件传输协议) 。
关于域名解析的小知识:域名解析,最先走是本地的hosts(C:WINDOWSsystem32driversetchosts)文件,解析失败了,才去访问DNS服务器解析、获取IP地址。通过hosts文件可以屏蔽垃圾网站内容弹出,例如:在hosts文件中添加,127.0.0.1 www.game18.com。
Socket就是为网络服务提供的一种机制。通信的两端都有Socket。网络通信其实就是Socket间的通信。数据在两个Socket间通过IO传输。
3、UDP协议
①:只要是网络传输,必须有socket 。
②:数据一定要封装到数据包中,数据包中包括目的地址、端口、数据等信息。
直接操作UDP很复杂,java语言将UDP封装成对象,易于我们的使用,这个对象就是DatagramSocket. 封装了UDP传输协议的socket对象。 因为数据包中包含的信息较多,为了操作这些信息方便,也一样会将其封装成对象。这个数据包对象就是:DatagramPacket.通过这个对象中的方法,就可以获取到数据包中的各种信息。 DatagramSocket具备发送和接受功能,在进行UDP传输时,需要明确一个是发送端,一个是接收端。
UDP的发送端:
①:建立udp的socket服务,创建对象时如果没有明确端口,系统会自动分配一个未被使用的端口。
②:明确要发送的具体数据。
③:将数据封装成了数据包。
④:用socket服务的send方法将数据包发送出去。
⑤:关闭资源。
UDP的接收端:
①:创建udp的socket服务,必须要明确一个端口,作用在于,只有发送到这个端口的数据才是这个接收端可以处理的数据。
②:定义数据包,用于存储接收到数据。
③:通过socket服务的接收方法将收到的数据存储到数据包中。
④:通过数据包的方法获取数据包中的具体数据内容,比如ip、端口、数据等等。
⑤:关闭资源。
1 import java.net.*; 2 //UDP发送端 3 class UDPDemoSend 4 { 5 public static void main(String[] args) throws Exception 6 { 7 8 System.out.println("发送端启动!"); 9 DatagramSocket ds=new DatagramSocket();//创建UDP服务 10 String str="Hello!UDP"; 11 //构建数据包 12 DatagramPacket dp=new DatagramPacket(str.getBytes(),str.length(),InetAddress.getByName("192.168.1.103"),10000); 13 //发送数据包 14 ds.send(dp); 15 //关闭socket服务 16 ds.close(); 17 } 18 } 19 //UDP接收端 20 class UDPDemoReceive 21 { 22 public static void main(String[] args) throws Exception 23 { 24 System.out.println("接收端启动!"); 25 DatagramSocket ds=new DatagramSocket(10000);//创建UDP服务,接收端要指定端口 26 byte[] buf=new byte[1024]; 27 DatagramPacket dp=new DatagramPacket(buf,buf.length);//创建数据包,接收端不用指定具体的接收端信息 28 ds.receive(dp);//接收数据。 29 String ip=dp.getAddress().getHostAddress();//获取IP 30 int port =dp.getPort();//获取端口 31 String data=new String(dp.getData(),0,dp.getLength());//获取数据 32 System.out.println(ip+":"+port+":"+data); 33 ds.close();//关闭数据流 34 } 35 }
运行结果如图所示:
由于UDP协议传输数据,只管发送数据,而不管接收端是否能够接收到数据。因此,应该首先启动接收端程序,再启动发送端程序。
下面写一个复杂一点的UDP程序,通过线程来做接收和发送,在同一个窗口中实现聊天。
1 import java.net.*; 2 import java.io.*; 3 //发送线程 4 class UDPSend implements Runnable 5 { 6 private DatagramSocket ds; 7 UDPSend(DatagramSocket ds) 8 { 9 this.ds=ds; 10 } 11 public void run() 12 { 13 DatagramPacket dp=null; 14 try 15 { 16 //创建控制台录入的流 17 BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); 18 for(String line=null;(line=br.readLine())!=null;) 19 { 20 //创建发送的数据包 21 dp=new DatagramPacket(line.getBytes(),line.length(),InetAddress.getByName("192.168.1.103"),10001); 22 ds.send(dp); 23 if(line.equals("886")) 24 break; 25 } 26 br.close(); 27 ds.close();//记得关流 28 } 29 catch (Exception e) 30 { 31 throw new RuntimeException(); 32 } 33 34 } 35 } 36 //接收线程 37 class UDPReceive implements Runnable 38 { 39 private DatagramSocket ds; 40 UDPReceive(DatagramSocket ds) 41 { 42 this.ds=ds; 43 } 44 public void run() 45 { 46 DatagramPacket dp=null; 47 try 48 { 49 while(true) 50 { 51 //BufferedReader br=new BufferedReader(new InputStream(System.in)); 52 byte[] buf=new byte[1024]; 53 dp=new DatagramPacket(buf,buf.length); 54 ds.receive(dp); 55 String ip=dp.getAddress().getHostAddress(); 56 int port =dp.getPort(); 57 String data=new String(dp.getData(),0,dp.getLength()); 58 System.out.println(ip+":"+port+"-"+data); 59 if(data.equals("886")) 60 System.out.println(ip+"退出聊天室!");//数据解析,如果是886就表明退出。 61 } 62 //ds.close(); 63 } 64 catch (Exception e) 65 { 66 throw new RuntimeException(); 67 } 68 69 } 70 } 71 class UDPDemo 72 { 73 public static void main(String[] args) throws Exception 74 { 75 DatagramSocket dsSend=new DatagramSocket(); 76 DatagramSocket dsReceive=new DatagramSocket(10001); 77 78 new Thread(new UDPReceive(dsReceive)).start(); 79 new Thread(new UDPSend(dsSend)).start(); 80 81 82 83 } 84 }
运行结果如下:
4、TCP协议
TCP传输
两个端点的建立连接后会有一个传输数据的通道,这通道称为流,而且是建立在网络基础上的流,称之为socket流。该流中既有读取,也有写入。
tcp的两个端点:一个是客户端,一个是服务端。
客户端:对应的对象,Socket
服务端:对应的对象,ServerSocket
TCP客户端:
①:建立tcp的socket服务,最好明确具体的地址和端口。这个对象在创建时,就已经可以对指定ip和端口进行连接(三次握手)。
②:如果连接成功,就意味着通道建立了,socket流就已经产生了。只要获取到socket流中的读取流和写入流即可,只要通过getInputStream和getOutputStream就可以获取两个流对象。
③:关闭资源。
TCP服务端:
服务端需要明确它要处理的数据是从哪个端口进入的。
当有客户端访问时,要明确是哪个客户端,可通过accept()获取已连接的客户端对象,并通过该对象与客户端通过IO流进行数据传输。
当该客户端访问结束,关闭该客户端。
接下来简单的演示一下客户端连接服务端的代码:
1 import java.net.*; 2 import java.io.*; 3 class TCPdemoClient 4 { 5 public static void main(String[] args) throws Exception 6 { 7 //创建socket服务 8 Socket s=new Socket("192.168.1.103",10002); 9 //获取输出流 10 OutputStream out=s.getOutputStream(); 11 out.write("哥们来了!".getBytes()); 12 //关闭socket,也同时关闭了流。 13 s.close(); 14 } 15 } 16 class TCPdemoServer 17 { 18 public static void main(String[] args) throws Exception 19 { 20 ServerSocket ss=new ServerSocket(10002); 21 Socket s=ss.accept();//获取连接到服务器的socket对象 22 String ip=s.getInetAddress().getHostAddress(); 23 System.out.println(ip+" connected!"); 24 InputStream in =s.getInputStream(); 25 byte[] buf =new byte[1024]; 26 int len=in.read(buf);//读取输入的数据 27 String data=new String(buf,0,len); 28 System.out.println(ip+" : "+data); 29 s.close();//关流 30 ss.close(); 31 } 32 }
结果如下:
注意:TCP协议传输数据必须先开服务端,再开客户端。否则,客户端根本连接不上服务端。
接下来写一个客户端和服务器端交互的例子,服务器端将客户端发送过来的数据转换成大写再发送回去,
1 import java.net.*; 2 import java.io.*; 3 class TCPdemoClient 4 { 5 public static void main(String[] args) throws Exception 6 { 7 //创建socket服务 8 Socket s=new Socket("192.168.1.103",10003); 9 //获取输出流,在tcp传输中注意结束标记和刷新流,所以用打印流很方便 10 PrintWriter out=new PrintWriter(s.getOutputStream(),true); 11 BufferedReader brInput=new BufferedReader( 12 new InputStreamReader(s.getInputStream())); 13 BufferedReader br=new BufferedReader( 14 new InputStreamReader(System.in)); 15 for(String line=null;(line=br.readLine())!=null;) 16 { 17 out.println(line); 18 if(line.equals("886")) 19 break; 20 String result=brInput.readLine(); 21 System.out.println(result); 22 } 23 br.close(); 24 //关闭socket,也同时关闭了流。 25 s.close(); 26 } 27 } 28 class TCPdemoServer 29 { 30 public static void main(String[] args) throws Exception 31 { 32 ServerSocket ss=new ServerSocket(10003); 33 Socket s=ss.accept();//获取连接到服务器的socket对象 34 String ip=s.getInetAddress().getHostAddress(); 35 System.out.println(ip+" connected!"); 36 BufferedReader br =new BufferedReader( 37 new InputStreamReader(s.getInputStream())); 38 //用打印流,自动刷新和换行,比较方便 39 PrintWriter pw =new PrintWriter(s.getOutputStream(),true); 40 for(String line=null;(line=br.readLine())!=null;) 41 { 42 System.out.println(ip+": "+line); 43 pw.println(line.toUpperCase()); 44 if(line.equals("886")) 45 System.out.println(ip+"退出聊天!"); 46 } 47 s.close();//关流 48 ss.close(); 49 } 50 }
运行结果如下图
1、上面练习中之所以客户端结束后,服务端也随之结束的原因在于:客户端的socket关闭后,服务端获取的客户端socket读取流也关闭了,因此读取不到数据,line = bufIn.readLine()为null,循环结束,ServerSocket的close方法也就执行关闭了。
2、上面练习中的客户端和服务端的PrintWriter对象out获取到数据后,一定要刷新,否则对方(服务端或客户端)就获取不到数据,程序便无法正常执行。刷新操作可以通过PrintWriter类的println()方法实现,也可以通过PrintWriter类的flush()方法实现。但是,由于获取数据的方法是BufferedReader对象bufIn的readLine()方法(阻塞式方法),此方法只有遇到“ ”标记时,才认为数据读取完毕,赋值给String对象line。所以,使用PrintWriter类的flush()方法刷新数据时一定要记得追加“ ”,标志行结束。
注意,在客户端向服务器端上传文件的时候,在上传文件完成的时候,客户端要给服务器一个信号文件传输完毕,否则只要流没有断开,服务器一直在阻塞在读流数据的函数中,这个结束标记可以是自定义的结束符(如:over),时间戳标记,最好的方法是s.shutdownOutput();调用socket方法关闭output流通知服务器传输完成。
现实中服务器都是通过多线程来响应不同的客户端的请求的,接下来就模拟一个客户端图片上传服务器多线程方法:
1 import java.io.*; 2 import java.net.*; 3 class UploadThread implements Runnable 4 { 5 private Socket s; 6 UploadThread(Socket s) 7 { 8 this.s=s; 9 } 10 public void run() 11 { 12 int count=0; 13 String ip=s.getInetAddress().getHostAddress(); 14 System.out.println(ip+"connected!"); 15 try 16 { 17 File f=new File(ip+".jpg"); 18 //如果文件已经存在 19 while(f.exists()) 20 { 21 f=new File(ip+"("+(++count)+").jpg"); 22 } 23 InputStream is=s.getInputStream(); 24 FileOutputStream fos=new FileOutputStream(f); 25 byte[] buf=new byte[1024]; 26 for(int len=0;(len=is.read(buf))!=-1;) 27 { 28 fos.write(buf,0,len); 29 } 30 OutputStream out=s.getOutputStream(); 31 out.write("上传成功".getBytes()); 32 fos.close(); 33 s.close(); 34 } 35 catch (Exception e) 36 { 37 throw new RuntimeException(); 38 } 39 } 40 } 41 class PicUploadServer 42 { 43 public static void main(String[] args) throws Exception 44 { 45 ServerSocket ss=new ServerSocket(10004); 46 while(true) 47 { 48 Socket s=ss.accept(); 49 new Thread(new UploadThread(s)).start(); 50 } 51 } 52 } 53 class UploadClient 54 { 55 public static void main(String[] args) throws Exception 56 { 57 Socket s=new Socket("192.168.1.103",10004); 58 FileInputStream fis=new FileInputStream("d:\1.jpg"); 59 //获取输出流,给服务器发送字节数据 60 OutputStream os=s.getOutputStream(); 61 byte[] buf=new byte[1024]; 62 for(int len;(len=fis.read(buf))!=-1;) 63 { 64 os.write(buf,0,len); 65 } 66 //告诉服务器数据发送完毕 67 s.shutdownOutput(); 68 //接收服务器的返回 69 InputStream is=s.getInputStream(); 70 byte[] bufIn=new byte[1024]; 71 int len = is.read(bufIn); 72 System.out.println(new String(bufIn,0,len)); 73 fis.close(); 74 s.close(); 75 } 76 }
图片上传成功了!
Socket类的构造函数中,有一个空参数的构造函数:
Socket()//通过系统默认类型的 SocketImpl创建未连接套接字对象
可以通过connect(SocketAddress endpoint)方法来连接服务器。而SocketAddress是一个抽象类,它的子类InetSocketAddress实现了IP套接字地址(IP地址+端口号)。所以就可以连接到服务器了。
5、URL和URLConnection
URL:统一资源定位符,也就是说根据URL能够定位到网络上的某个资源,它是指向互联网“资源”的指针。
方法:
1)构造函数:URL(String protocol,String host,int port,String file);//根据指定 protocol、host、port号和 file 创建 URL对象。
2)String getProtocol();//获取协议名称
3)String getHost();//获取主机名
4)int getPort();//获取端口号
5)String getFile();//获取URL文件名
6)String getPath();//获取此URL的路径部分
7)String getQuery();//获取此URL的查询部,客户端传输的特定信息
URLConnection方法:
1)URLConnection openConnection();//用URL调用此方法,返回一个 URLConnection 对象,它表示到 URL 所引用的远程对象的连接。
2)InputStream getInputStream();//获取输入流
3)OutputStream getOutputStream();//获取输出流
1 import java.net.*; 2 import java.io.*; 3 class URLDemo 4 { 5 public static void main(String[] args)throws Exception 6 { 7 String strURL="http://192.168.1.100:8080/myweb/hello.html?name=admin"; 8 URL u=new URL(strURL); 9 System.out.println("getProtocol: "+u.getProtocol()); 10 System.out.println("getHost: "+u.getHost()); 11 System.out.println("getPort: "+u.getPort()); 12 System.out.println("getFile: "+u.getFile()); 13 System.out.println("getPath: "+u.getPath()); 14 System.out.println("getQuery: "+u.getQuery()); 15 URLConnection uc=u.openConnection(); 16 InputStream is=uc.getInputStream();//等价于InputStream is=u.openStream(); 17 byte[] buf=new byte[1024]; 18 int len=is.read(buf); 19 System.out.println(new String(buf,0,len)); 20 is.close(); 21 } 22 }
输出结果为:
通过URL获取的流,接收到服务器的响应信息,URLConnection对象已经把响应头给解析了,只取到了消息体的数据,如果需要消息头的信息,URLConnection.getHeaderField(“具体的属性名”);来获取。
网络编程内容总结到这里就结束了,对于网络编程而言,重要的是理解其步骤,按照步骤的需要,一步步搭建根基! 客户端和服务端需要交互,那么就要构建相对应的流,供其输入和输出! 对于阻塞式方法,一定要注意,提供停止标签! 对于PrintWriter ,记得用println而不是write;不要忘了加上true,自动刷新!