Java网络编程学习笔记
1. 网络编程基础知识
1.1 网络分层图
网络分层分为两种模型:OSI模型以及TCP/IP网络模型,前者模型分为7层,是一个理论的,参考的模型;后者为实际应用的模型,具体对应关系见下图:
1.2 网络编程三要素之IP地址
目前的版本是ipv4,采用的是点分十进制的表示方式(dotted-decimal notation),一共4位,每一位表示一个字节,因为IP地址是没有负数的,因此表数范围是0-255,子网掩码的存在使得一个IP地址被区分为了两个部分,网络地址以及主机地址,原理如下:比如一个子网掩码为255.255.255.0,实际上相当于将整个地址的前三位划分成了网络地址,而最后一位划分成了主机地址,网络地址越少,主机位越多,可分配的主机数量就越多,IP地址的分类图如下:
一般企业使用的是C类网址,校园网使用的是B类网址,而A类网址往往是*,国家使用
1.3 IP地址编程实战——InetAddress类
查看该类的API可知,它的构造方法是私有的,因此无法通过new的方式创建对象,而是通过getByName这一静态方法获取到实例对象的:
/* 演示InetAddress类的基本API */ public class InetAddressDemo { public static void main(String[] args) throws Exception { //1.通过给定IP地址的方式获取到InetAddress对象 InetAddress ip1 = InetAddress.getByName("192.168.153.1"); System.out.println(ip1); //2.通过给定主机名的方式获取到InetAddress对象 InetAddress ip2 = InetAddress.getByName("DESKTOP-LEPR355"); System.out.println(ip2); //3.通过获取到的InetAddress解析出主机名 System.out.println(ip1.getHostName()); //4.通过获取到的InetAddress解析出主机的IP地址 System.out.println(ip2.getHostAddress()); } }
结果显示:解析成功!
// 控制台中的解析结果如下: /192.168.153.1 DESKTOP-LEPR355/192.168.153.1 DESKTOP-LEPR355 192.168.153.1
1.4 网络编程三要素之端口(port)
端口号是用来唯一表示计算机上运行着的进程的,以一个数字进行指定,该数字的范围是0-65535共65536个,其中系统进程使用的是0-1023,因此应用程序只能使用后面的数字,否则会发生冲突,使得应用程序运行失败
1.5 网络编程三要素之协议(Protocal)
两个主机之间想要相互通信,必须要使用相同的语言,对于数据的格式,错误控制,重传机制等的一系列规范就是协议
1.5.1 UDP编程
UDP是User Datagram Protocal的缩写:用户数据报协议,它的特点是不用建立连接,发送端只管发送数据给接收端而不必考虑接收端是否真的接收到了数据,数据是被封装成数据包的,限制在64K,数据包自己知道它应该去哪一个主机的哪一个端口,因为UDP编程的这一特性,使得它的发送效率高,速度快,通常可以用在发送短信或是网络电话,网络会议等场景上,发送端和接收端的编程步骤如下所示:
其中receive方法是一个阻塞式方法,没有信息发送过来,此方法就会一直处于等待状态
使用同一台主机上的两个端口模拟UDP编程编写一个简易聊天程序
信息发送端代码:
import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Scanner; /* 使用UDP编程模拟一个简易聊天程序,发送端输入"bye"退出程序 使用同一台主机的两个不同的端口模拟两台不同的主机 */ public class DatagramSocketSender { public static void main(String[] args) throws Exception { //新建一个DatagramSocket对象 DatagramSocket ds = new DatagramSocket(); //新建一个扫描器 Scanner sc = new Scanner(System.in); while(true){ //接收用户键入信息 System.out.println("Please input your message: "); String line = sc.nextLine(); //判断用户输入的是不是"bye" if(line.equals("bye")){ break; }else{ //获取输入信息字符串的字节数组对象 byte[] bys = line.getBytes(); DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("localhost"), 8888); ds.send(dp); } } ds.close(); } }
信息接收端代码:
import java.net.DatagramPacket; import java.net.DatagramSocket; /* 模拟简易聊天程序的接收端,一般情况下,接收端看成是服务器,是不会轻易关闭的 */ public class DatagramSocketReceiver { public static void main(String[] args) throws Exception { //绑定端口 DatagramSocket ds = new DatagramSocket(8888); byte[] bys = new byte[64 * 1024]; DatagramPacket dp = new DatagramPacket(bys, bys.length); //服务端一般来说不会关闭 while(true){ //接收来自发送端的数据 ds.receive(dp); //进行解析 byte[] data = dp.getData(); int len = dp.getLength(); System.out.println(new String(data,0,len)); } } }
使用UDP编程发送文件
文件发送端代码:
import java.io.BufferedReader; import java.io.FileReader; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /* 使用UDP编程模拟文件的发送端 需要注意的是,因为要让文件接收端知道什么时候文件发送完成,需要写一个结束标记符 */ public class FileSender { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new FileReader("e:/test/a.txt")); DatagramSocket ds = new DatagramSocket(); String line = null; while((line = br.readLine()) != null){ byte[] bys = line.getBytes(); DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("localhost"), 8888); ds.send(dp); } //文件已经全部发送完,最后发送一个结束标记,"886" byte[] bys = "886".getBytes(); DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("localhost"), 8888); ds.send(dp); //关闭资源 br.close(); ds.close(); } }
文件接收端代码:
import java.io.BufferedWriter; import java.io.FileWriter; import java.net.DatagramPacket; import java.net.DatagramSocket; /* 使用UDP编程模拟文件接收方,当接收到字符"886"时表示文件已经发送完成,退出 */ public class FileReceiver { public static void main(String[] args) throws Exception { BufferedWriter bw = new BufferedWriter(new FileWriter("e:/test/aa.txt")); //指定端口 DatagramSocket ds = new DatagramSocket(8888); //封装数据包 byte[] bys = new byte[64 * 1024]; DatagramPacket dp = new DatagramPacket(bys,bys.length); //解析接收到的数据并将其写出到另一个文件中去 while(true){ //接收数据 ds.receive(dp); byte[] data = dp.getData(); int len = dp.getLength(); String line = new String(data,0,len); if(line.equals("886")){ break; }else{ bw.write(line); bw.newLine(); bw.flush(); } } //关闭资源 bw.close(); ds.close(); } }
上述代码是对文本文件进行操作,因为是按行进行读取,因此可以将文件的每一行数据封装成一个数据包,但是,如果操作的是图片,视频,mp3等非文本文件,就需要对代码进行更改了
图片发送端代码
import java.io.File; import java.io.RandomAccessFile; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /* UDP编程模拟发送一张图片 */ public class PictureSender { public static void main(String[] args) throws Exception { DatagramSocket ds = new DatagramSocket(); //使用RandomAccessFile,因为可以移动指针 RandomAccessFile raf = new RandomAccessFile("e:/test/picture.png", "rw"); //新建一个字节数组进行数据拷贝,大小应小于64k,因为要存放一些控制字符 int size = 60 * 1024; byte[] bys = new byte[size]; int fileLen = (int) new File("e:/test/picture.png").length(); int count = fileLen % size == 0 ? fileLen / size : fileLen / size + 1; for(int i = 0; i < count; i++){ int len = size; if(fileLen % size != 0 && i == count - 1){ len = fileLen % size; } raf.seek(i * size); byte[] buf = new byte[len]; raf.read(buf); //封装数据包 DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 8888); ds.send(dp); } raf.close(); ds.close(); } }
图片接收端代码
import java.io.FileOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; /* 使用UDP编程模拟图片接收端 */ public class PictureReceiver { public static void main(String[] args) throws Exception { FileOutputStream fos = new FileOutputStream("e:/test/picture1.png"); //指定端口 DatagramSocket ds = new DatagramSocket(8888); byte[] bys = new byte[64 * 1024]; DatagramPacket dp = new DatagramPacket(bys, bys.length); while(true){ ds.receive(dp); byte[] data = dp.getData(); int len = dp.getLength(); fos.write(data,0,len); } } }
1.5.2 TCP编程
TCP是Transmission Control Protocal的缩写,俗称的TCP/IP中,IP协议用于唯一标识两个主机,而TCP协议用于确保传输完整可靠,它在网络两端各建立一个Socket,从而在通信的两端形成一个虚拟的网络连接,一旦连接建立,就可以在两端进行通信了,而Socket网络通信底层使用的是IO技术
UDP编程与TCP编程的几个区别总结:
1. UDP没有客户端,服务端之分,只有发送端和接收端,两端之间无需建立连接,而在TCP编程中明确地对于客户端和服务端进行了区分
2. 如果客户端指定的IP地址不存在,就会报UnknownHostException异常;而如果是端口号不存在,那么就会报Connection refused异常,在客户端创建套接字对象时需要明确指定两者,不可缺其一,而UDP编程并不会
3. 在UDP编程中,需要给接收端发送一个结束标记让接收端知道何时应该退出循环,而TCP编程由于底层使用的仍然是IO,因此只需要判断是否读到-1或是null就可以了
TCP编程实战:将一个本地文件发送到本机的另一个端口上去
客户端:
import java.io.*; import java.net.Socket; /* 使用TCP编程模拟将一个文件传输到另一个端口上的客户端 */ public class TCPClient { public static void main(String[] args) throws Exception { //新建一个Socket对象,指定需要发送到的IP地址和端口 Socket socket = new Socket("localhost", 8888); //新建一个输入流读取本地文件 BufferedReader br = new BufferedReader(new FileReader("e:/test/a.txt")); //通过Socket对象新建一个输出流,并用转换流进行包装 OutputStream os = socket.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); //开始按行读取并写出 String line = null; while((line = br.readLine()) != null){ bw.write(line); bw.newLine(); bw.flush(); } //关闭资源 bw.close(); os.close(); br.close(); socket.close(); } }
服务端:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; /* TCP编程模拟服务端接收文件 */ public class TCPServer { public static void main(String[] args) throws Exception { //绑定端口 ServerSocket ss = new ServerSocket(8888); //调用accept方法接收数据,是一个阻塞式方法 Socket socket = ss.accept(); //使用ss创建输入流 InputStream is = socket.getInputStream(); //进行包装 BufferedReader br = new BufferedReader(new InputStreamReader(is)); BufferedWriter bw = new BufferedWriter(new FileWriter("e:/test/aa.txt")); String line = null; while((line = br.readLine()) != null){ bw.write(line); bw.newLine(); bw.flush(); } //关闭资源 is.close(); br.close(); bw.close(); socket.close(); ss.close(); } }
1.5.3 流的半关闭
场景说明:现在需要对上述代码进行改良,在服务端接收完来自客户端的数据之后,再给客户端发送一个反馈消息,告诉客户端数据接收完毕:
客户端:
import java.io.*; import java.net.Socket; /* 使用TCP编程模拟将一个文件传输到另一个端口上的客户端 改进:并接收来自服务端的反馈消息 */ public class TCPClient2 { public static void main(String[] args) throws Exception { //新建一个Socket对象,指定需要发送到的IP地址和端口 Socket socket = new Socket("localhost", 8888); //新建一个输入流读取本地文件 BufferedReader br = new BufferedReader(new FileReader("e:/test/a.txt")); //通过Socket对象新建一个输出流,并用转换流进行包装 OutputStream os = socket.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); //开始按行读取并写出 String line = null; while((line = br.readLine()) != null){ bw.write(line); bw.newLine(); bw.flush(); } //将流半关闭 socket.shutdownOutput(); //新建一个输入流来接收来自服务端的反馈消息 InputStream is = socket.getInputStream(); byte[] buf = new byte[1024]; int len = is.read(buf); System.out.println(new String(buf,0,len)); //关闭资源 is.close(); bw.close(); os.close(); br.close(); socket.close(); } }
服务端:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; /* TCP编程模拟服务端接收文件 改进:并给客户端发送反馈消息 */ public class TCPServer2 { public static void main(String[] args) throws Exception { //绑定端口 ServerSocket ss = new ServerSocket(8888); //调用accept方法接收数据,是一个阻塞式方法 Socket socket = ss.accept(); //使用ss创建输入流 InputStream is = socket.getInputStream(); //进行包装 BufferedReader br = new BufferedReader(new InputStreamReader(is)); BufferedWriter bw = new BufferedWriter(new FileWriter("e:/test/aa.txt")); String line = null; while((line = br.readLine()) != null){ bw.write(line); bw.newLine(); bw.flush(); } //接收完文件之后,再新建一个输出流告诉客户端文件接收完毕 OutputStream os = socket.getOutputStream(); os.write("文件接收完毕!".getBytes()); //关闭资源 os.close(); is.close(); br.close(); bw.close(); socket.close(); ss.close(); } }
如果在客户端代码中不添加socket.shutdownOutput();这句代码,会发生两端相互等待的情况,这是因为:客户端将文件发送到了末尾,并不会通过流将最后的null传给服务端,因此进入到了等待状态,而服务端一直没有接收到null值,以为还有数据传送过来,在socket对象没有关闭之前就一直在循环等待接收文件,而shutdownOutput方法相当于在把socket关闭之前,先把由socket对象创建的输出流先关闭了,从而避免了相互等待的情况出现,因此这个又被称为是流的半关闭