黑马程序员_java基础视频第23天_ java网络编程

 ---------------------- android培训、java培训、期待与您交流! ----------------------

                                                                                                            java网络编程

网络通讯的三要素: IP地址,端口号,传输协议

网络模型:

计算机网络是指由通信线路互相连接的许多自主工作的计算机构成的集合体,各个部件之间以何种规则进行通信,就是网络模型研究的问题。网络模型一般是指OSI七层参考模型和TCP/IP四层参考模型。这两个模型在网络中应用最为广泛。

网络模型分为OSI模型和TCP/IP模型,如下图:

他的原理如下所示:

获取IP地址:
IP是网络中设备的标识,因为不易于记忆可以使用主机名称,
本地主机地址:127.0.0.1,名称:localhost
static InetAddress
getLocalHost() 得到本地主机
static InetAddress getByName(String host) 通过主机名称得到主机
String getHostName() 得到主机的名称
 String getHostAddress() 得到主机的地址
static InetAddress [] getAllByName(String host) 根据名称得到多个主机
1,获取本机的主机:
	public static void getLocalHost() throws UnknownHostException {
		InetAddress i = InetAddress.getLocalHost();
		// 打印InetAddress
		System.out.println(i);
		// 打印主机名称
		System.out.println("name:" + i.getHostName());
		// 打印主机地址
		System.out.println("address:" + i.getHostAddress());
	}


输出结果:

johnny/192.168.1.102

name:johnny
address:192.168.1.102
2,获取远程的主机,如百度:
	public static void getWANHost1() throws UnknownHostException {
		InetAddress i = InetAddress.getByName("www.baidu.com");
		System.out.println(i);
	}


控制台输出:www.baidu.com/119.75.217.56

有因为.百度的主机可能不只一台,

 

	public static void getWANHost() throws UnknownHostException {
		InetAddress[] i = InetAddress.getAllByName("www.baidu.com");
		for (int j = 0; j < i.length; j++) {
			System.out.println(i[j]);
		}
	}
控制台输出:
www.baidu.com/119.75.218.45
www.baidu.com/119.75.217.56
端口号:
用于标识进程的逻辑地址,
有效的端口,0~65535,其中0~1024是系统使用或者保留的端口
 
传输协议:
通讯的规则, 常用的有TCP和UDP协议
下面来介绍一下这两个协议的特点和区别:
UDP协议:
1,将数据及源和目的的封装成数据包中,不需要建立连接
2,每一个数据包的大小限制在64K内;
3,因为无连接,是不可靠协议;
4,不需要建立连接,速度快
 
TCP:
1,建立连接,形成传输数据的通信
2,在连接中进行大数据的传输;
3,通过三次握手完成连接,是可靠连接;比如你和张三连接,第一次问张三是否在,第二次张三收到告诉你在,第三次告诉张三你知道了
4,必须建立连接,效率会略低.
 
Socket
1,Socket是为网络服务提供的一种机制;
2,通信的两端都是Socket;
3,网络通信就是Socket间的通信;
4,数据在两个Socket间通过IO传输;
 
为了帮助理解Socket,我在网上搜索了一段文件来解说:
        socket非常类似于电话插座。以一个国家级电话网为例。电话的通话双方相当于相互通信的2个进程,区号是它的网络地址;区内一个单位的 交换机 相当于一台 主机 ,主机分配给每个用户的局内号码相当于socket号。任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤消连接。
 
java操作Socket常用的有
java.lang.Object
  java.net.DatagramSocket
这个类描述了为发送和接受数据报包的Socket,常用的构造方法有
DatagramSocket(int port) 
DatagramSocket(int port, InetAddress laddr) 
既然该类提供了发送和接受的功能,那么他就提供了这样的方法
receive(DatagramPacket p) 接受数据,把接受到的数据封装大DatagramPacket 中,这个方法是一个阻塞方法,也就是如果没有接受大数据,就会一直等待
send(DatagramPacket p) 发送数据包
java.lang.Object
  java.net.DatagramPacket
该类用于别用于非连接状态下,邮寄数据服务,
atagramPacket(byte[] buf, int length, InetAddress address, int port) 
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 
 
现在我们来完成一小程序,使用 UDP协议,来发送一个数据包:

 

	public static void main(String[] args) throws Exception {
		// 1 ,创建UDP服务,创建DatagramSocket对象
		DatagramSocket socket = new DatagramSocket();
		// 2,确定数据并封装成数据报包
		byte[] buffer = "I'm yuzhiqiang".getBytes();
		//往端口13200,主机为"192.168.1.102"发送buffer字节数组里面的数据
		DatagramPacket packet = new DatagramPacket(buffer, buffer.length,
				InetAddress.getByName("192.168.1.102"), 13200);
		// 通过服务发送数据
		socket.send(packet);
		// 关闭资源
		socket.close();
	}

}


运行程序,发现与我们以前运行的程序,没有任何差别,我们发送的数据到哪里去了呢?
因为端口13200的程序没有开启,这样的话数据就丢失了.
下面我们来定义一个接受数据包的程序:

 

	public static void main(String[] args) throws Exception {
		DatagramSocket socket = new DatagramSocket(13200);
		byte[] buffer = new byte[1024];
		DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
		socket.receive(packet);
		//通过数据包获取其中的数据
		String ip = packet.getAddress().getHostAddress();
		String data = new String(packet.getData(),0,packet.getLength());
		//获取是从哪个端口发过来的 
		int port = packet.getPort();
		System.out.println(ip+"--"+data+"--"+port);
		socket.close();
	}


我们打开cmd命令,现在我们不能先运行发送数据包的程序,而是运行接收数据包的程序,因为如果接收数据包的程序没有启动发送的数据就会丢失

运行 UdpReceive.java如图:

 

 

两个程序运行完毕后,任然没有看到我们要发送的数据,为什么呢?因为我们是向端口13200,发送数据,而接受的数据的程序不一定是13200端口的程序
所以我们必须把接收程序的端口设置成13200,而不是由系统自动分配端口:
把UdpReceice.java的
DatagramSocket socket = new DatagramSocket();改为
DatagramSocket socket = new DatagramSocket(13200);
重新编译后, 再次按照上面的顺序运行这两个程序:
程序阻塞等待发送:

 

发现阻塞的程序发生了变化,输出的数据正式我们发送的!并且还有对方法的端口和IP.
当这个对方的端口也是系统生成的,那么我们能不能指定他呢?
只要把发送端的程序改为:
 DatagramSocket socket = new DatagramSocket(9999);
再次 运行:发现63169变为了9999

 

我们知道我们发过去的数据实在程序硬编码的,我们能不能在键盘上输入文字在发过去呢?而在接收端不停的接收呢?
发送端:
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		DatagramSocket socket = new DatagramSocket();
		String line = null;
		while ((line = br.readLine()) != null) {
			if ("886".equals(line)) {
				break;
			}
			byte[] buffer = line.getBytes();
			// 如果把ip主机设置成192.168.1.255,那就相当于发送广播,在这个网段内活动的主机都可以收到数据
			DatagramPacket packet = new DatagramPacket(line.getBytes(),
					buffer.length, InetAddress.getByName("192.168.1.102"),
					11111);
			socket.send(packet);
		}

		socket.close();
	}


接收端
	public static void main(String[] args) throws Exception {
		DatagramSocket socket = new DatagramSocket(11111);
		while(true){
			byte[] buffer = new byte[1024];
			DatagramPacket p = new DatagramPacket(buffer,buffer.length);
			socket.receive(p);
			
			String ip = p.getAddress().getHostAddress();
			String data = new String(p.getData(),0,p.getLength());
			int port = p.getPort();
			System.out.println(ip);
			System.out.println("\t\t"+data+"\t"+port);
		}
	}


在接收端,我们能不能在把

DatagramSocket socket =new DatagramSocket(11111);放进while循环里面呢?
我们来测试一下,
发送数据:

 

 

 

为什么?当第一次循环的时候,创建了端口为11111的服务程序,因为在接收端,是死循环,所以当我发送数据过去后,输出信息,它重新执行循环里面的代码,又创建了一个口为11111的服务程序,所以出现端口正在被使用的错误!
 
接下来测试程序
发送端

 

从上可以看出我们启动了两个cmd命令窗口,我们能不能在一个窗口发信息,也能在同一个窗口收到信息呢?
上面的程序我们有两个进程,现在一个进程里面实现,那么就要用到多线程!
我们首先创建两个线程,一个线程用于发数据,一个线程用于接收数据,两个线程互不影响.例如下面的程序:

 

class Send implements Runnable {
	private DatagramSocket socket;

	public Send(DatagramSocket socket) {
		this.socket = socket;
	}

	public void run() {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String line = null;
		try {
			while ((line = br.readLine()) != null) {
				if ("886".equals(line))
					break;
				byte[] buff = line.getBytes();
				//注意192.168.1.255的意义
				DatagramPacket packet = new DatagramPacket(buff, buff.length,
						InetAddress.getByName("192.168.1.255"), 11111);
				socket.send(packet);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			socket.close();
		}
	}

}


class Receive implements Runnable {
	private DatagramSocket socket;

	public Receive(DatagramSocket socket) {
		this.socket = socket;
	}
	public void run() {
		while (true) {
			byte[] buffer = new byte[1024];
			DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

			try {
				socket.receive(packet);
				String ip = packet.getAddress().getHostAddress();
				String data = new String(packet.getData(), 0, packet
						.getLength());
				System.out.println(ip);
				System.out.println("\t" + data);

			} catch (IOException e) {
				e.printStackTrace();
			} 

		}
	}

}


测试程序:

public class ChatTest {

	public static void main(String[] args) throws SocketException {
		DatagramSocket receive = new DatagramSocket(11111);
		DatagramSocket send = new DatagramSocket();
		new Thread(new Send(send)).start();
		new Thread(new Receive(receive)).start();
	}
}


结果如图:

 

 

上面是基于UDP进行数据传输的,下面来看一下,TCP协议的数据传输:
 
1,TCP分为客户端和服务器端
2,客户端对应的对象是Socket
   服务器端对应的对象是ServerSocket.
 
一个客户端对象包含输入输出流,通过输出流往服务器发送数据,通过输入流读取数据,我们知道一个服务器可以与多个客户端连接,那么服务端本应该发送数据给A,那么它不会发送到B这个客户端吗?它靠什么来区分的呢?因为一个客户端和服务器连接,服务器就可以得到Socket对象,就可以得到Socket的输入输出流,它要和特定的客户端互动,就会使用该Socket的输入输出流,这样就不会错了!看下面一个原理图:

 

现在我们来模拟一下这样的程序:
//服务器端
public class TcpDemo {

	public static void main(String[] args) throws IOException {
		//建立一个服务端的Socket服务,并监听(绑定)10001端口
		ServerSocket server = new ServerSocket(10001);
		//获取连接到的客户端对象,此方法是阻塞式方法,因为没有客户端连接 ,就一直等待
		Socket client = server.accept();
		String ip = client.getInetAddress().getHostAddress();
		//打印连接服务器的客户端IP
		System.out.println(ip+"connected...");
		InputStream is = client.getInputStream();
		byte[] buff = new byte[1024];
		int len = is.read(buff);
		System.out.println(new String(buff,0,len));
		client.close();
		//只服务一次
		server.close();
	}
}


//客户端
class Client {
	public static void main(String[] args) throws Exception {
		// 创建客户端的Socket服务,并指定主机和端口
		Socket client = new Socket("192.168.1.102", 10001);
		// 得到输出流,往服务端发送数据
		OutputStream os = client.getOutputStream();
		os.write("你好".getBytes());
		client.close();
	}
}


注意这注意,我们首先要运行服务端的程序,因为客户端一开始就要和服务器端连接,如果服务器端都没有,那么就不存在连接的问题的,就像我们连接百度,如果百度的服务器都没有开启,我们就没有办法连接了.
运行服务端代码:程序进入阻塞状态:

 

 

面的程序,服务端收到信息,并没有给客户端反馈,现在的需求是服务器端收到数据后,要向客户端发送数据,
现在该怎么做呢?因为Socket有输入输出流,所以服务端可以通过Socket的输出流来完成.

 

//服务器端
public class TcpDemo2 {

	public static void main(String[] args) throws IOException {
		ServerSocket server = new ServerSocket(22223);
		Socket client = server.accept();
		String ip = client.getInetAddress().getHostAddress();
		System.out.println(ip+".....connected!");
		InputStream is = client.getInputStream();
		byte[] buff = new byte[1024];
		int len = is.read(buff);
		String message = new String(buff, 0, len);
		System.out.println(message);
		
		OutputStream os = client.getOutputStream();
		os.write(("我已经收到你信息:"+"\""+message+"\"").getBytes());
		client.close();
		server.close();
	}
}
//客户端
class Client2 {
	public static void main(String[] args) throws Exception {
		Socket client = new Socket("192.168.1.102", 22223);
		OutputStream os = client.getOutputStream();
		// 向服务器发数据
		os.write("你好啊!".getBytes());
		InputStream is = client.getInputStream();
		byte[] buff = new byte[1024];
		// 读取服务器发来的数据/read方法是阻塞式方法,没有读到数据就会等
		int len = is.read(buff);
		System.out.println(new String(buff, 0, len));
		client.close();
	}
}




 

下面我们来看一下实际应用中常见的问题,我们以一个例子展开:
 
需求:建立一个文件转换服务器,客户端给服务器发送文本,服务器将文本转成大写再返回给客户端;
而且客户端可以不断的向服务器发送数据,当客户端输入over时,转换结束.
分析:这需要我们使用到键盘录入,字符缓冲区等IO操作:
代码如下:
public class TcpTask {

	public static void main(String[] args) throws IOException {
		// 监听或绑定23456端口
		ServerSocket server = new ServerSocket(23456);
		// 获取客户端对象
		Socket client = server.accept();

		BufferedReader buffIn = new BufferedReader(new InputStreamReader(client
				.getInputStream()));
		BufferedWriter buffOut = new BufferedWriter(new OutputStreamWriter(
				client.getOutputStream()));
		String line = null;
		while ((line = buffIn.readLine()) != null) {
			buffOut.write(line);
			System.out.println(line);
		}
		client.close();
		server.close();
	}
}

 
  
class Client3 {
	public static void main(String[] args) throws Exception {
		// 获取服务器对象
		Socket client = new Socket("192.168.1.102", 23456);
		// 首先要获取键盘录入, 转换流就不再介绍了
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String line = null;
		BufferedWriter buffOut = new BufferedWriter(new OutputStreamWriter(
				client.getOutputStream()));

		BufferedReader buffIn = new BufferedReader(new InputStreamReader(client
				.getInputStream()));
		while ((line = br.readLine()) != null) {
			if ("over".equals(line))
				break;
			buffOut.write(line);
			String serverLine = buffIn.readLine();
			System.out.println(serverLine.toUpperCase());
		}
		br.close();
		client.close();
	}
}按照步骤运行上面的程序
服务器端正在阻塞


客户端正在等我输入:

输入数据后,

发现服务端任然没有什么变化,并且客户端也无法再次输入数据.为什么会出现这么奇怪的现象呢?

 首先我们来看两段代码 

 客户端代码片段

		while ((line = br.readLine()) != null) {
			if ("over".equals(line))
				break;
			buffOut.write(line);
			String serverLine = buffIn.readLine();
			System.out.println(serverLine.toUpperCase());
		}

服务端代码片段:

		while ((line = buffIn.readLine()) != null) {
			buffOut.write(line);
			System.out.println(line);
		}
1, 看到上面的代码我们不难发现,我们在往服务器端发数据和服务器端往客户端发数据的时候使用的是字符缓冲区的流,但是我们并没有刷新,此时数据任然在内存中.所以应该调用flush()方法.
重新运行 ,但是还是没有把问题解决.


2  我们看第二个代码片段的while循环,line = buffIn.readLine()说明服务器端正在读取客户端发来的数据,我们在前面学过,readLine()方法是阻塞式方法,并且只有遇到换行符的时候就不再阻塞,然后客户端发来的数据,并不包含换行符,所以这个方法一直在阻塞,所以我们在客户端代码出添加一行代码:
buffOut.newLine();如:
		while ((line = br.readLine()) != null) {
			if ("over".equals(line))
				break;
			buffOut.write(line);
			buffOut.newLine();
			buffOut.flush();
			String serverLine = buffIn.readLine();
			System.out.println(serverLine.toUpperCase());
		}
同理 在服务端处,也要这样做,如
		while ((line = buffIn.readLine()) != null) {
			buffOut.write(line);
			buffOut.newLine();
			buffOut.flush();
			System.out.println(line);
		}


重新编译,再运行结果如图:

服务端

客户端:

 

 

 
总结:
我们在使用缓冲区的流诸如BufferedXX,我们要注意flush()刷新问题!
要明白readLine()与read()方法的区别
1,readLine()是缓冲区流对象的方法,而read()不是;
2,read()当我们使用字节输出流读取键盘录入的数据时,他会把换行符也会读取到,而readLine()方法不会读取换行符
3,read和readLine都属于阻塞式方法.就拿上面的例子来说,因为服务器可能在使用客户端是输出流写数据,所以在客户端读取数据的时候,就会出现阻塞,因为他不知道什么时候服务器端不在写数据了.因为readLine在读取一行的时候并没有读到换行符,所以他认为还在写数据.

 

查看readLine()的API:

 

意思是说该方法,读取文本的一行数据,一行的结尾被认为是\r\n,当然其他的系统就不同了,它返回的是 行的内容,不包含行终止符,当到达流的尾端就返回null
 
我们可以编写一个小程序验证一下
		public static void main(String[] args) throws Exception {
			InputStream is = System.in;
			OutputStream os = new FileOutputStream("test3.txt");
			byte[] buff = new byte[10];
			int len = is.read(buff);
			System.out.println(len);
			os.write(buff, 0, len); 
			os.close();
		} 


 

然后我们来测试一下readLine()
	public static void main(String[] args) throws Exception {
		// 字符缓冲流
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		OutputStream os = new FileOutputStream("test3.txt");
		String line = br.readLine();
		System.out.println(line.length());
		os.write(line.getBytes());
	}


 

下面一个程序使用TCP复制一个文件文件,来引出 标记 的概念

 

public class TcpCopyFile {

	public static void main(String[] args) throws Exception {

		ServerSocket ss = new ServerSocket(65432);

		Socket socket = ss.accept();
		String ip = socket.getInetAddress().getHostAddress();
		System.out.println(ip+".....connected!");
		// 用该对象读取客户端发来的信息
		BufferedReader br = new BufferedReader(new InputStreamReader(socket
				.getInputStream()));
		// 把读到的内容保存到指定 的文件中
		PrintWriter pw = new PrintWriter(new FileWriter("testCopy.txt"), true);
		String line = null;
		while ((line = br.readLine()) != null) {
			pw.println(line);
		}
		// 把信息反馈给客户端
		PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
		out.println("上传完毕");
		out.close();
		pw.close();
		socket.close();
		ss.close();
	}
}
 
class Client4 {

	public static void main(String[] args) throws Exception {
		// 创建客户端服务
		Socket socket = new Socket("192.168.1.102", 65432);
		// 读取test.txt文件
		BufferedReader br = new BufferedReader(new FileReader("test.txt"));
		// 把文件的内容发送到客户端
		PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
		String line = null;
		while ((line = br.readLine()) != null) {
			pw.println(line);
		}
		// 读取服务器发送的反馈信息
		BufferedReader buffIn = new BufferedReader(new InputStreamReader(socket
				.getInputStream()));
		String message = buffIn.readLine();
		System.out.println(message);
		//关闭资源
		buffIn.close();
		pw.close();
		br.close();
		socket.close();
	}
}



 

来看一下两个代码片段:
客户端:
		while ((line = br.readLine()) != null) {
			pw.println(line);
		}
		// 读取服务器发送的反馈信息
		BufferedReader buffIn = new BufferedReader(new InputStreamReader(socket
				.getInputStream()));
		String message = buffIn.readLine();
服务端:
		while ((line = br.readLine()) != null) {
			pw.println(line);
		}
		// 把信息反馈给客户端
		PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
		out.println("上传完毕");
当客户端把数据全部发送到了,执行带 String message = buffIn.readLine(); 处,程序处于阻塞状态,
在服务器端,把客户端发来的数据全部写入了 testCopy.txt文件,但是他并不知道客户端是否还会通过OutputStream发数据,所以客户端line = br.readLine()一直处于阻塞状态等待读取数据.所以服务器反馈的信息代码出没法执行可以通过
Socket的socket.shutdownOutput();告诉服务器端,我不在传输数据了
		while ((line = br.readLine()) != null) {
			pw.println(line);
		}
		//当发完数据后,告诉服务器端,我不在传输数据了
		socket.shutdownOutput();


程序运行正常,当然我们也可以通过自定义标记,如在客户端发送一条数据(比如"over"),告诉服务器数据已经传输完毕,然后服务器端就break,当然如果如果文本文件中存在over字符那么,数据还没有读完就会break,导致数据不完整!

 

 

 

 

 ---------------------- android培训、java培训、期待与您交流! ----------------------

 

 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
 

你可能感兴趣的:(黑马程序员)