Java Socket网络编程

329 阅读10分钟

 1、网络相关概念

1.1、网络

​编辑

1.2、网络模型

​编辑

​编辑

​编辑

​编辑

1.3、网络三要素

1.3.1、IP地址

处于网络中的通信设备(电脑、手机、电视等),都会分配一个ip地址。这些设备在通信之前,首先要接入到网络中。通过通信设备的网卡。而这个网卡属于一个硬件,并且所有的网卡在出厂的时候,都有一个固定的网卡地址。并且这个网卡地址(MAC地址)全球唯一。

在dos窗口中输入 ipconfig /all 可以查询ip(mac地址)相关的详细信息

​编辑

所有的网卡都有一个唯一的物理地址,但是这个地址非常的不好记。于是给每个网卡有分配了一个逻辑(IP)地址。

这个逻辑地址就是ip地址。通信的过程中,就可以根据这个逻辑地址找到处于网络中的这台设备。

IP地址也是对处于网络中的某个通信终端的标识。然后通信的另外一方,就可以根据这个ip地址找到对方,和对方进行通信。

由于IP地址也不容易记忆,因此又给每个ip绑定了一个名称。这个名称被称为(域名)。

通信设备只要连接到网络中,网络就会给这个设备分配一个IP地址。要进行网络通信,必须使用的IP地址。

通信设备能够联网,就必须要有网卡,每个网卡有一个物理地址(MAC地址,是唯一的)。

但是每个网卡在出厂的时候,就已经绑定死了一个IP地址,本地回环地址(127.0.0.1)。

小常识:经常使用ping命令,来检测网卡的好坏。

1.3.2、域名解析

访问网络的时候,真正是需要通过IP地址访问。但是由于IP地址也不容易记忆。因此又给网络中的每个IP地址起名字,这个名称就叫做域名。

当我们在浏览器等可以访问网络的软件中输入了某个域名之后,需要将这个域名解析(解释)成对应的IP地址。这个过程就叫域名解析。

域名解析分成2步:

  1. 本地解析:

在我们的操作系统中,有一个hosts文件,当输入域名之后,首先会在hosts文件中找有没有当前的域名对应的IP地址,有就会使用这个IP地址。

C:\Windows\System32\drivers\etc\hosts

  1. DNS服务器解析:

如果第一步解析失败了,会到网络中的DNS服务器上进行解析。DNS服务器中会将全球所有的域名和IP设置在其中。如果DNS服务器解析失败,说明当前的域名有问题的。

1.3.3、协议

协议:通信双方通信的时候需要遵守的通信的规则。后期我们会遇到很多协议(高级协议,应用协议:http、ftp、https )。

目前我们网络编程介绍的协议属于底层协议,所有的高级协议都是基于底层协议。

有ip可以找到网络中的那个设备,设备之间要通信,还要使用相同的通信协议。

传输层协议:

TCP:传输控制协议。Transmission Control Protocol 传输控制协议

TCP协议在通信的时候,要求通信的双方先建立起连接(面向有连接的协议)。在建立连接的过程中需要经过三次握手才能完成连接的建立。

​编辑

由于TCP协议需要经过三次握手,才能建立连接,因此TCP协议传输数据的时候比较安全,可靠。但是效率较低。

因此在互联网中很多对安全要求较高的程序底层全部使用TCP协议。        

UDP:用户数据报文包协议。UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。它是面向无连接的协议。发送一方,不用关心接受一方是否在线。就直接发送数据。如果对方在就可以接受数据,如果对应不在,这时数据就自动的被丢弃。

UDP协议不安全,不可靠,但是效率高。不能传输大数据。

1.3.4、端口

我们通过ip可以找到网络中具体那个通信终端的设备。而具体需要访问这个设备中的哪个资源(软件)信息,这时由于设备中运行的资源(软件)肯定很多,这时每个资源(软件)必须再给一个唯一的编号(标识)。通过这个标识才能保证我们可以没有错误的访问到指定ip地址的具体那个资源(软件)上。

端口:是一台设备(电脑、手机登)上某个应用程序的唯一标识。设备中运行的任何一个软件,只要启动,就肯定会有一个唯一的编号其整个软件绑定。

端口从0开始到65535之间。0到1024之间的端口数字已经分配给本机的操作系统的应用程序占用,因此后期我们书写程序如果需要绑定端口,这时必须大于1024.

总结:ip 是网络中某个设备的标识。端口是设备中某个应用程序的标识,协议是设备之间通信的规则。

2、网络编程

2.1、IP对象

在网络中需要连接到另外一个的设备,然后才能进行通信。而处于网络中的任何一个设备都需要唯一的数字地址标识(IP)。

IP是网络中设备中存在的一类特殊的数据,那么Java针对IP整个特殊的数据,也给出了一个类。

Java中的网络编程相关的类保存在java.net包下:

​编辑

InetAddress类没有对外提供构造函数,其中提供静态的方法可以根据指定的主机(域名)找到对应的IP,然后把IP封装成InetAddress对象。

​编辑

/*
 * 演示IP对象
 */
public class IPDemo {
	public static void main(String[] args) throws UnknownHostException {
		
		/*
		 * InetAddress类没有提供工作方法,需要使用静态方法获取对象
		 * 	getByName(String host) 
		 *  String host:参数可以是字符串形式的ip地址,也可以域名,也可以电脑(主机)的名字
		 */
		//InetAddress inet = InetAddress.getByName("欣心");
		InetAddress inet = InetAddress.getByName("www.qq.com");
		String ip = inet.getHostAddress();
		System.out.println(ip);
		
		/*
		 * getAllByName(String host) :
		 *  有时一个域名下面对应的是多个IP地址。getAllByName方法可以获取到指定的域名下面的多个ip地址对象。
		 */
		InetAddress[] inets = InetAddress.getAllByName("www.baidu.com");
		for (InetAddress ia : inets) {
			System.out.println( "IP = "+ia.getHostAddress() );
		}
	}
}

2.2、通信的端点介绍

要进行网络通信,需要在网络最少有2个端点(连接到网络的设备)。

每个端点都称为一个Socket(网络套接字)对象。而通信的两端(socket)之间,是通过网络在进行连接。

​编辑

2.3、UDP编程

如果要进行网络编程,前提必须先确定具体使用哪个协议进行通信。使用UDP,还是TCP协议。Java针对两种协议,给出了不同的类,可以完成网络编程。

网络编程的那些类在java.net包下:

​编辑

​编辑

DatagramSocket:它的底层使用的就是UDP协议,它表示的基于UDP协议的通信的端点对象。

基于UDP在两端之间通信的数据被称为数据报包(DatagramPacket)。

​编辑

​编辑

2.3.1、发送方

/*
 * 演示基于UDP协议的编程:
 */
public class UdpSend {
	public static void main(String[] args) throws IOException {
		/*
		 *  创建通信的端点对象
		 *  DatagramSocket() : 
		 *  创建出来的端点对象,JVM会根据操作系统目前可以分配的端口号给当前端点一个端口号。
		 */
		DatagramSocket ds = new DatagramSocket( );
		
		/*
		 * 需要创建负责对数据打包的对象
		 * DatagramPacket:需要创建的是用来发送数据的包裹
		 * DatagramPacket(byte[] buf, int length, InetAddress address, int port) 
		 * 	byte[] buf : 需要被打包的字节数据
		 * 	int length : 需要打包的字节个数
		 * 	InetAddress address:是接收方的IP对象
		 *  int port : 是接收方的端口号
		 */
		byte[] buf = "udp通信!!!".getBytes();
		int length = buf.length;
		InetAddress inet = InetAddress.getByName("192.168.31.8");
		int port = 6789;
		DatagramPacket dp = new DatagramPacket( buf , length , inet , port );
		
		/*
		 * 发送数据
		 */
		ds.send(dp);
		
		// 关闭端点对象
		ds.close();
		
	}
}

2.3.2、接收方

/*
 * UDP的接收端
 */
public class UdpReceive {
	public static void main(String[] args) throws IOException {
		
		/*
		 *  创建端点对象:
		 */
		DatagramSocket ds = new DatagramSocket( 6789 );
		
		/*
		 * 创建拆包的对象
		 * DatagramPacket(byte[] buf, int length) 
		 * 	byte[] buf : 创建拆包对象的时候构造方法中的数组,是用来存储拆包之后的数据。
		 * 	int length : 可以接受的字节的长度,不能超过数组的长度
		 */
		DatagramPacket dp = new DatagramPacket( new byte[1024] , 1024 );
		
		/*
		 * 接收数据
		 */
		ds.receive(dp);  // 如果接受不到数据,就会停留在此。阻塞式方法
		/*
		 * 需要获取拆包之后的数据:拆包对象自己肯定知道具体接受了多少个字节的数据,
		 * 以及数据来自哪个IP对象,和哪个端口
		 */
		// 获取数据
		byte[] bs = dp.getData();
		// 获取数据长度
		int len = dp.getLength();
		// 获取发送数据方的IP地址
		String ip = dp.getAddress().getHostAddress();
		// 获取发送端的端口号
		int port = dp.getPort();
		System.out.println( "数据来自 :" + ip + ", 发送数据的端口号是:" + port +" 发送的数据是: " + new String( bs , 0 , len ) );
		
		// 关闭接收端
		ds.close();
	}
}

2.4、TCP编程

TCP进行通信之前,必须先建立双方的通信通道。

TCP编程中的对象:

         客户端clinet:主要是需要和服务器建立连接,然后客户端就可以给服务端发送数据,或者接受服务端的数据。

客户端永远是主动的去连接服务端,而服务端不会主动的找客户端。

         服务端server:它是给各个客户端提供服务的。它是在被动的等待不同的客户端连接,只要每个客户端连接到服务端之后,

服务端就会为这个连接上的客户端建立它们两个之间的通信的通道

如果还有其他的客户端也连接到了服务端,那么服务端会与每个客户端有各自的通信通道。彼此不会影响。

不管是客户端,还是服务端(服务器)它们只要有一方将通道给断开了,那么彼此的通信立刻结束。

Java针对TCP的客户端对象:Socket;服务端对象:ServerSocket。

2.4.1、基于TCP的客户端

​编辑

​编辑

/*
 * 演示基于TCP协议的客户端实现
 */
public class TcpClientDemo {
	public static void main(String[] args) throws IOException {
		/*
		 * Socket(String host, int port) 
		 * String host : 客户端需要连接的服务端的ip地址
		 * int port : 客户端需要连接的服务端的端口号
		 * 
		 * 当我们创建好Socket对象之后,Socket就会尝试连接到指定的IP和端口上。
		 */
		Socket s = new Socket( "192.168.31.8" , 6789 );
		/*
		 * TCP的Socket对象,客户端给服务端发送数据,需要使用字节输出流
		 * 如果客户端接收服务端发来的数据,需要使用字节输入流
		 */
		// 基于客户端和服务端的通信的通道获取流对象,然后和服务端之间发送和接收数据
		OutputStream out = s.getOutputStream();
		// 写数据
		out.write("TCP简单通信!!!".getBytes());
		
		// 关闭客户端
		s.close();
	}
}

2.4.2、基于TCP的服务端

​编辑

​编辑

/*
 * 基于TCP协议的服务端实现
 */
public class TcpServerDemo {
	public static void main(String[] args) throws IOException {
		/*
		 * ServerSocket(int port) 
		 * int port : TCP服务端的端口号
		 */
		ServerSocket ss = new ServerSocket( 6789 );
		
		/*
		 * 当创建好服务端对象之后,需要调用服务端中的accept方法,
		 * 来接收某个通过网络连接到服务端的那个客户端。
		 * 并在服务端的内部会得到连接上来的那个客户端对象。
		 */
		Socket s = ss.accept();
		/*
		 * 在服务端的内部可以这个连接上来的这个客户端进行通信
		 * 
		 * 需要在服务端的内部使用连接到的这个客户端对象
		 * 和真正的客户端进行通信
		 */
		InputStream in = s.getInputStream();
		
		byte[] buf = new byte[1024];
		int len = 0 ;
		// 这里没有使用while循环读取数据,这里使用数组只读取了一次数据
		len = in.read(buf);
		// 打印数据
		System.out.println( new String( buf , 0 , len ) );
		
		// 关闭客户端
		s.close();
		// 关闭服务端
		ss.close();
	}
}

2.4.3、TCP协议两端解释

​编辑

TCP的服务端ServerSocket其实它仅仅只是在调用accept方法获取连接的Socket对象,然后在服务端内部依然在使用Socket获取流对象,和客户端的Socket进行数据交互。

2.4.4、TCP服务端开启多线程

/*
 * 基于TCP协议的服务端实现  , 将服务端改造成多线程,为每个客户端都可以提供服务
 */
public class TCPServer2 {
	public static void main(String[] args) throws Exception {
		
		// 创建服务端对象
		ServerSocket ss = new ServerSocket( 8989 );
		/*
		 * 服务端开启死循环的目的是让服务端永远都无法停止,
		 * 这样可以保证只要有客户端连接进来,
		 * 服务端就会立刻给这个客户端开启线程并进行服务
		 */
		while( true ){
			/*
			 *  创建好服务端对象之后,等待客户端的连接,
			 *  只要有一个客户端连接,就开启一个线程,
			 *  让服务端中的这个线程和当前这个客户端进行数据交互
			 */
			Socket s = ss.accept();
			System.err.println("有一个客户端连接到服务端了.............");
			
			new Thread( new Runnable(){
				public void run(){
					try {
						// 在run方法中书写当前需要给客户端提供的服务代码
						InputStream is = s.getInputStream();
						
						byte[] buf = new byte[1024];
						int len = 0;
						// 这里我们先不用循环读取
						len = is.read(buf);
						
						// 打印数据
						String  data = new String( buf , 0 , len );
						System.err.println( data );
						// 在服务端关闭和当前这个客户端的连接
						s.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}).start();
			
			
			// 关闭服务端
			//ss.close();
		}
	}
}

2.5、TCP的练习

​编辑

2.5.1、客户端实现

/*
 * 上传文件的客户端,同时需要获取服务端发来的数据
 */
public class UploadClient {
	public static void main(String[] args) throws IOException {
		
		// 创建客户端对象
		Socket s = new Socket( "192.168.31.8" ,6799 );
		
		// 创建字节输入流,读取本地的图片数据
		FileInputStream fis = new FileInputStream("e:/1.jpg");
		
		// 获取输出流,给服务端发图片
		OutputStream out = s.getOutputStream();
		System.out.println("客户端准备给服务端发送数据......");
		byte[] buf = new byte[1024];
		int len = 0;
		/*
		 * 客户端在使用循环读取本地的图片,然后将数据写给服务端
		 * 但是由于客户端的循环读取到文件的末尾,循环就会停止
		 * 但是文件已经读取结束的信息并不会发送给服务端
		 */
		while( ( len = fis.read(buf) ) != -1 ){
			out.write(buf, 0, len);
		}
		// 程序到这里说明已经将图片数据读写完成。 
		/*
		 *  需要人为的告诉服务端文件数据发送完成了
		 *  使用Socket类中的shutdownOutput,禁用输出流
		 *  本质是在告诉接收方数据写完了,不要在继续等待读取数据
		 */
		s.shutdownOutput();
		
		// 需要关闭自己new的流对象
		fis.close();
		System.out.println("客户端将数据全部发送给服务端......");
		// 获取服务端发来的数据
		InputStream in = s.getInputStream();
		byte[] b = new byte[1024];
		int length = 0;
		/*
		 * 下面的read方法在读取服务端发来的数据,如果读取不到数据,
		 * 客户端的程序就会卡在read的地方,卡住原因是因为没有读取到数据
		 */
		length = in.read(b);
		System.out.println( new String( b , 0 , length ));
		
		// 关闭客户端
		s.close();
		
	}
}

2.5.2、服务端实现

/*
 * 上次文件的服务端
 */
public class UploadServer {
	public static void main(String[] args) throws IOException {
		
		// 创建服务端对象
		ServerSocket ss = new ServerSocket(6799);
		
		// 获取连接到服务端的客户端对象
		Socket s = ss.accept();
		System.out.println("有客户端连接到服务端.........");
		// 获取输入流读取图片数据
		InputStream in = s.getInputStream();
		// 创建输出流
		FileOutputStream fos = new FileOutputStream("e:/upload/1.jpg");
		byte[] buf = new byte[1024];
		int len = 0;
		/*
		 * 服务端使用了循环在读取客户端发来的图片数据,由于没有读取到结束的标记,
		 * 导致这个循环无法停止。
		 * 在字节输入或字符输入流中,read方法如果读取到末尾返回的都是-1
		 * 现在循环无法停止,说明read就没有读取到-1.
		 */
		while( (len = in.read(buf)) != -1 ){
			fos.write(buf, 0, len);
		}
		System.out.println("服务端将图片保存完成了.....");
		// 程序到这里,说明服务端已经将图片写到文件中
		fos.close();
		// 给客户端发送数据
		OutputStream out = s.getOutputStream();
		out.write("图片上传成功!!!".getBytes());
		// 关闭客户端
		s.close();
		// 关闭服务端
		ss.close();
	}
}

2.5.3、解决并发上传问题

public class UploadPicServer2 {

	public static void main(String[] args) throws IOException {
		// 创建服务器端对象
		ServerSocket ss = new ServerSocket(9999);

		while (true) {
			// 获取客户端对象
			Socket s = ss.accept();
			new Thread(new Service(s)).start();
		}
	}
}

class Service implements Runnable {

	Socket s = null;

	public Service(Socket s) {
		this.s = s;
	}

	public void run() {
		try {
			// 获取连接的客户的IP
			String ip = s.getInetAddress().getHostAddress();

			// 从客户端获取输入流
			InputStream in = s.getInputStream();

			// 创建输出的路径对象
			File dir = new File("e:\img");
			if (!dir.exists()) {
				dir.mkdir();
			}
			int count = 1;

			// 创建输出的文件对象
			File filePic = new File(dir, ip + "" + count + ".jpg");
			while (filePic.exists()) {
				count++;
				filePic = new File(dir, ip + "" + count + ".jpg");
			}
			// 创建文件输出流对象
			FileOutputStream fis = new FileOutputStream(filePic);
			byte[] buf = new byte[1024];
			int len = 0;
			while ((len = in.read(buf)) != -1) {
				fis.write(buf, 0, len);
			}

			OutputStream out = s.getOutputStream();
			out.write("上传成功".getBytes());

			fis.close();
			s.close();
		} catch (IOException e) {
		}
	}
}

3、网络编程相关类

3.1、URL和URI

3.3.1、URL类

​编辑

URL:统一资源定位符。当我们需要访问网络中某个设备中的具体某个资源(文件、图片、音频、视频等等)的时候,需要输入这个资源所在的设备的ip、端口、协议、资源路径等信息才能够访问。

例如:访问京东的某个图片:

         img11.360buyimg.com:80/da/jfs/t116…

上面的这个路径就被称为一个URL对象。URL就是浏览器地址栏中输入的东西。

当有一个URL存在的时候,我们就可以知道这个资源具体在哪个IP的设备上的某个端口下,并且可以获取到这个资源的全部真实的路径。

我们可以使用URL去访问这个资源。

一个URL所表示的资源有下面几部分组成:

         协议:https/http ,这个超文本传输协议,属于应用级别的协议(底层依然在使用TCP协议)

         主机:img11.360buyimg.com ,其实就域名,在DNS解析(域名解析)之后对应就是IP地址。

         端口:80 ,底层需要使用TCP连接服务端时使用的端口号。

         资源和资源所在的路径:jfs/t11689/182/937825045/142913/f6207e5a/59fc0cbaNe5148bb2.jpg

​编辑

/*
 * 演示URL使用
 */
public class URLDemo {
	public static void main(String[] args) throws MalformedURLException {
		
		// 创建URL对象  key=value&key=value
		URL url = new URL("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=url&rsv_pq=ed0e930100017019&rsv_t=45b82LvipAe4VamiI2yEmFS8BfzDEkLmvXSX5QKXeW2QuINdHFk5FncbZi8&rqlang=cn&rsv_enter=1&rsv_sug3=3&rsv_sug1=2&rsv_sug7=100");
		
		System.out.println("getDefaultPort = " + url.getDefaultPort() );
		System.out.println("getPort = " + url.getPort() );
		System.out.println("getProtocol = " + url.getProtocol() );
		System.out.println("getHost = " + url.getHost() );
		System.out.println("getFile = " + url.getFile() );
		System.out.println("getPath = " + url.getPath() );
		System.out.println("getQuery = " + url.getQuery() );
		
	}
}

3.1.2、URI类

​编辑

URL:定位符,URI标识符。两个都是用来表示网络中的资源的。如果URL确定了,那么资源就是唯一的。而URI仅仅表示资源,但是资源具体在哪里,是不确定的。

确定的URL:img11.360buyimg.com/da/jfs/t116…

不确定的URI:59fc0cbaNe5148bb2.jpg  ,它表示就是一个资源,但是资源在网络中的什么地方,无法确定。

                             jfs/t11689/182/937825045/142913 这个资源也不确定

URI其实就是URL中的某一部分。但是URI所表示的范围比URL大。

3.2、URLConnection

​编辑

URLConnection类:它表示的URL所代表的资源(服务端)与客户端之间的连接的通道。

要想得到URLConnection对象,必须使用URL类中的openConnection方法:

​编辑

当程序中得到URLConnection对象之后,就表示得到客户端和服务端之间的连接通道,就可以通过流可以完成客户端和服务端之间的数据读写操作。

​编辑

/*
 * 演示使用URL去访问网络中的资源
 */
public class URLConnectionDemo {
	public static void main(String[] args) throws IOException {
		
		// 创建URL对象
		URL url = new URL("https://img11.360buyimg.com/da/jfs/t11689/182/937825045/142913/f6207e5a/59fc0cbaNe5148bb2.jpg");
		
		// 打开和URL所表示的资源所在的服务器之间的连接
		URLConnection conn = url.openConnection();
		
		// 获取输入流
		InputStream in = conn.getInputStream();
		
		// 创建输出流
		FileOutputStream fos = new FileOutputStream("e:/jd.jpg");
		// 读取数据
		byte[] buf = new byte[1024];
		int len = 0 ;
		while( ( len = in.read(buf) ) != -1 ){
			// 写数据
			fos.write(buf, 0, len);
		}
		// 关流
		fos.close();
	}
}