JAVA Class24
学习内容:
1.网络通信协议
(1)TCP/IP协议:
TCP/IP协议中的四层分别是应用层、传输层、网络层和链路层
链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。
客户端与服务端的三次握手:
第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
第三次握手,客户端再次向服务器端发送确认信息,确认连接。
优缺点:每次连接都要经过三次握手,速度慢,优势在于可以保证传输数据的完整性。
(2)UDP协议:
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
优缺点:数据收发不确认,可能会造成数据的不完整,优势在于速度更快。
(3)IP与端口:
IP版本分为IPV4 IPV6,现阶段用户量最大的是IPV4,IP地址是网络上的客户端的身份标识,而端口是用来访问客户端的应用程序。形象的来说,IP地址就是小区名称,而端口则是门牌号。
(4)JAVA的IP地址封装:
InetAddress类,常用方法:
public class Test { public static void main(String[] args) { try { InetAddress inet = InetAddress.getLocalHost(); System.out.println(inet.getHostAddress());//本机IP地址 System.out.println(inet.getHostName());//本机名 System.out.println(InetAddress.getByName("USER-20180226VT"));//在局域网内,根据主机名获取IP,返回一个InetAddress对象,参数可填主机名或者IP地址 } catch (Exception e) { // TODO: handle exception } } }
2.UDP协议通信:
收发信息的载体:DatagramPacket,常用构造方法有两个:
new DatagramPacket(byte数组,发送字节长度,InetAddress对象,整型端口号);//用于通过指定IP的指定端口号收发数据
new DatagramPacket(byte数组,发送字节长度);//不指定IP、端口号
常用方法:getAddress() 获取ItnetAddress对象 getPort()获取端口号 getLength()获取数据字节长度 getData()获取数据,返回字节数组
上述方法一般用于接收端,读取发送端的信息
收发信息的对象:DatagramSocket,常用构造方法:
new DatagramSocket() 不指定端口号
new DatagramSocket(整型端口号) 指定端口号
常用方法: send()发送数据 receive()接收数据 close() 关闭
public class UDPSend { public static void main(String[] args) throws IOException { Scanner s = new Scanner(System.in); DatagramSocket dgs = new DatagramSocket();//没指定端口号,JVM自动分配 InetAddress inet = InetAddress.getByName("192.168.1.255"); while(true) { System.out.println("请输入内容:"); String mess = s.next(); byte[] send =mess.getBytes(); DatagramPacket dgp = new DatagramPacket(send,send.length,inet,8888); //注意这里的8888是信息发往的端口,发送用的端口是JVM分配的 dgs.send(dgp); } //dgs.close(); } } public class UDPReceive { public static void main(String[] args) throws IOException { DatagramSocket dgs = new DatagramSocket(8888); while(true) { byte[] receive = new byte[1024*64]; DatagramPacket dgp = new DatagramPacket(receive,receive.length); dgs.receive(dgp); String ip = dgp.getAddress().getHostAddress(); int port = dgp.getPort(); int length = dgp.getLength(); byte[]data = dgp.getData(); String s = new String(data,0,length); System.out.printf("发送方地址为:%s 发送方端口号为:%d 数据长度为:%d 数据为:%s \n",ip,port,length,s); } } }
3.TCP通信
由于TCP协议的三次握手机制,所以严格区分服务端和客户端,通过输出、输出字节流来进行数据收发
(1)服务端
ServerSocket 两种构造方式 无参不指定端口,有参指定端口
服务端无法通过Socket对象获取输入输出流,要通过accept()方法返回一个Socket对象 :Socket s = ss.accept();//返回的是客户端的Socket对象
这里要注意,如果用equals来判断客户端的Socket对象与服务端通过accept()对象返回的Socket,会返回false,虽然接收端口相同,但发送端口不同,例如
public class Test { public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(9008); Socket s = new Socket("127.0.0.1",9008); Socket ns = ss.accept(); //不是同一个是对象: System.out.println("我是客户端"+s); //Socket[addr=/127.0.0.1,port=9008,localport=51515] System.out.println("我是服务端"+ns); //Socket[addr=/127.0.0.1,port=51515,localport=9008] } }
(2)客户端
Socket 构造器均为有参,new Socket(String host, int port) new Socket(InetAddress address, int port),常用的时第一种
Socket通过getInputStream()、getOutputStream() 获取输入输出流
聊天小程序:
收发线程:
public class SendThread implements Runnable{ private Socket s; public SendThread(Socket s){ this.s = s; } public void run(){ try { OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); while(true){ Scanner sc = new Scanner(System.in); String str = sc.next(); dos.writeUTF(str); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public class ReceiveThread implements Runnable{ private Socket s; public ReceiveThread(Socket s) { this.s = s; } public void run() { try { InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); String ip = s.getInetAddress().getHostAddress(); int port = s.getLocalPort(); while (true) { String msg = dis.readUTF(); System.out.println("来自:"+ip+"端口:"+port+"的消息:\n"+msg); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
服务端与客户端分别开一个收发线程:
public class TCPClient { public static void main(String[] args) { try { Socket s = new Socket("127.0.0.1",8888); SendThread st = new SendThread(s); ReceiveThread rt = new ReceiveThread(s); new Thread(st).start(); new Thread(rt).start(); } catch (IOException e) { e.printStackTrace(); } } } public class TCPServer { public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(8888); System.out.println("服务器正在监听8888端口"); Socket s = ss.accept();//返回的是客户端的Socket对象 SendThread st = new SendThread(s); ReceiveThread rt = new ReceiveThread(s); new Thread(st).start(); new Thread(rt).start(); } catch (IOException e) { e.printStackTrace(); } } }
多线程数据上传:
注意shutdownOutput这个方法,当socket关闭时也会有同样效果,如果客户端在上传数据后还要再接收服务端返回的信息,则必须使用该方法,最后在关闭socket,通知服务端数据传输完毕,客户端输出流关闭,自动flush,服务端读取的字节数最终被确定!此时while循环的-1条件可以成立,同时服务端的输入流也会随之关闭。
如果不使用该方法,服务端while循环-1条件会一直不满足,因为客户端的输入流一直在等待服务端传来的数据,socket也没法关闭,导致数据没法flush,而服务端一直在等待读取客户端传来的数据,双方等到海枯石烂...结果导致堵塞,所以在socket关闭前必须使用shutdownOutput这个方法,先关闭客户端的输出流,服务端得以接收到数据,然后服务端再把文字信息输出给客户端,客户端的输入流接收到信息并打印,最后socket关闭,整个流程执行完毕!
注意,如果客户端只是上传文件,然后不再接受服务端传来的消息,则可以直接关闭socket,这样输出流自动关闭!不必调用该方法
另外:如果不关闭socket,只是输出流手动flush,那么数据可以上传,但是服务端读取有问题,导致图片出现问题!
同理,shutdownInput也有类似的效果!
public class UploadThread implements Runnable{ private Socket s; public UploadThread(Socket s){ this.s = s; } public void run(){ File f = new File("d:\\test\\1366768.jpg"); try (FileInputStream fis = new FileInputStream(f); OutputStream os=s.getOutputStream(); ){ byte[] upload = new byte[(int)f.length()]; fis.read(upload); os.write(upload); s.shutdownOutput();//关闭上传客户端的输出流,通知服务端客户端的数据传输完毕,服务端的输入流也随之关闭 InputStream is = s.getInputStream();//不关闭,is在等待读取新的输出流的数据 byte[] mess = new byte[1024]; int len=is.read(mess); System.out.println(new String(mess,0,len)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { s.close();//关闭socket,所有流随之关闭
//如果在这里不关闭socket,服务端不在发消息,客户端也不接受,那么结果是输出流没关闭,没有上传
//如果输出流不关闭,只用flush,数据会全部上传,但是服务端读取不完整,图片有问题!
} catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
public class ServerThread implements Runnable{ private Socket s; public ServerThread(Socket s) { this.s = s; } Object o = new Object(); public void run() { synchronized(o) { File f = new File("d:\\upload"); String file =f.getPath()+File.separator+System.currentTimeMillis()+".jpg"; try(FileOutputStream fos = new FileOutputStream(file);){ InputStream is = s.getInputStream(); byte[] read = new byte[1024]; int a = 0; if(!f.exists()) { f.mkdirs(); } while((a=is.read(read))!=-1) { fos.write(read,0,read.length); } System.out.println("结束!"); s.getOutputStream().write("上传成功".getBytes()); //注意这里,因为服务端又新开了一个输出流,导致客户端一定要关闭之前的输出流,终止输出,
//使得服务端循环的-1条件成立,以此通知服务端文件上传完了,
//与此同时服务端的输入流也随之关闭,
//而新增的这个输出流与客户端用来接收这个输出流的输入流会在socket关闭时关闭,
//如果没有这个输出流,客户端只是上传文件、之后不再接收服务端传来的消息,
//只需最后关闭socket就好! } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }