黑马程序员——网络编程

------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

 

网络是个很庞杂的系统,内部分为许多层次:


每个层都有自己的协议(通信规则)比如说主机至网络层就有网线 光纤 蓝牙 红外 WiFi等等。

网际层最常见的协议是IP(InternetProtocol)

传输层最常见的协议是:TCP(TransportControl Protocol)和UDP。

应用层协议:HTTP://www.  FTP

 

学网络编程主要在网际层和传输层混(用什么网线不是我们管的)

以后做JavaWeb开发,在应用层。

 

主机与主机之间要建立通讯具体分为以下几个步骤

1.      找到对方IP

2.      数据要发送到对方指定的应用程序上。为了标识这些应用程序,我们给这些网络应用程序都用数字进行标识。为了方便称呼这个数字,叫做端口,它是逻辑的端口,而不是网线插槽。

3.      定义通信规则。这个通信规则称为协议。国际组织定义了通用协议TCP/IP,当然还有其他协议,但是两台机器必须使用相通的协议才能通信,所以很多保密单位使用自己特有的协议,普通机器因为没有这种通信规则根本入侵不进去。

 

IP地址是由4个0~255的数字构成的。可以理解为4个字节。

本地回环地址:127.0.0.1(用来ping测试网卡是否正常)

机器配地址的时候有几个地址不能用:最后一位是.0和.255,前者代表一个网络地址,网段。后者代表这个段里面的广播地址,向这里发的数据全部本网段的机器都能收到。

端口:可以自己配置0~65535(二进制的16位)    但是0~1024被系统程序保留了。(没必要两个机器上的端口都配成一样的,比如说一个机器上两个软件端口冲突了,其中一个端口自动往后顺延就可以了,还是能接收到发过来的数据)

 

网络编程作为一个复杂的事物Java也定义了一系列的类来方便对它们进行操作。全部在java.net包里面。

InetAddress类

该类没有构造函数,可以通过以下方法获取到本类对象

static InetAddressgetByName(String host) 传入一个主机名,获取一个InetAddress对象。

static InetAddress[]getAllByName(String host)  传入一个主机名,返回来一个数组,里面装着所有对应的InetAddress对象。

static InetAddressgetLocalHost()  获取本机的InetAddress对象。效果等同于:

InetAddress.getByName("127.0.0.1")或者InetAddress.getByName ("localhost ")

 

但是以上获取方法会产生一种异常UnknownHostException未知目标主机异常,是说有可能获取不到目标主机,IP地址可能是错的,甚至getLocalHost的话如果本地没有装网卡也会有问题,所以必须处理或者抛出这个异常。

同时,该类提供了一些方法

StringgetHostAddress()  获取该对象的IP地址

StringgetHostName()  获取该对象的主机名

构造方法里的传参Stringhost可以填写如www.baidu.com或者”192.168.0.123”或者”localhost”,也就是说域名或者IP地址都可以。

我们在使用的时候,以IP地址为主,因为主机名还需要解析,慢,而且比如说一个局域网

 

上没有主机名和IP地址的映射关系暴露出来的话,那么主机名就解析不出来,主机名那里还是会显示IP地址。

 

除了TCP外还有一种比较常见的传输协议是UDP。TCP与UDP的主要区别如下:(必须掌握)

UDP

是面向无连接的。(不管对方在不在都发送,在就送到,不在就发丢,数据发送之前不用建立连接)

UDP在发送之前会把数据打成数据包,而且需要明确对方的地址和端口。

发的包有限制(一次不能超过64KB,所以会自动把大数据分成很多小包)

因为面向无连接所以不可靠,而优点是速度快。(局域网聊天软件就是UDP,屏幕分享软件,网络视频会议用的也是UDP,偶尔丢几帧没关系)都是力求速度而对数据丢失无所谓。而下载用的是UDP,不用实时的来看,所以对速度要求不是那么强烈,但是对准确性要求极高,不能出现任何丢包。

UDP的机制有点像邮寄包裹,不知道对方地址存在不存在都可以发。

TCP

面向连接,对方必须在。对方在才发送。

通过三次握手来完成,是可靠协议。(A:你听的见我吗?B:听得见,你听得见我吗?A:听得见),这样A和B才能都确定双方的来回两条链路是畅通的)

三次握手后完成TCP传输通道的建立。这样数据就能在传输通道里面传输了。

可靠,但较慢,每次建立连接都消耗资源,但是可以大数据量传输,而不用打成有一定容量限制的包,直接在通路里面走就可以了。

 

TCP可以理解为电话,满足两个条件:拨的号码正确,而且拨通了才能说。

UDP可以理解为对讲机,随便哪个合法频段,不管对方在不在都能说。

 

Socket(插座,插槽,针脚,也有的翻译叫套接字):两台机器间通信的端点。

网络编程指的就是Socket编程,而网络通信其实就是Socket间的通信。

Socket就是为网络服务提供的一种机制。

通信的两端都必须先得有Socket,因为有了它之后才能进行连接,这样数据才能有传输的通路。(可以看成是机器里面应用程序的码头,先有码头,才能有航线)Socket是通信的端点。

数据在两个Socket间通过IO传输。

Socket可以看成是抽象的逻辑的网口插槽。【注意区分】Socket不等同于我们上面提到的端口,它的功能与端口类似,但含义比端口更广,因此Socket的构造函数一般都需要提供端口。

 

每个传输协议都有不同的建立自己Socket的方式。

UDP连接实例

UDP的Socket用的是DatagramSocket(发送和接收都是,用构造函数区别),它里面发送的对象是DatagramPacket数据包,数据包的构造函数有两种一种是带InetAddress address和int port的,这种是发送数据包,另外一种不带,是接收数据包。UDP的Socket都需要指定自己的地址,然后建立数据包,在包上规定目标的IP和端口,再通过Socket的方法发送出去,然后接收也是依靠Socket的方法接收,收到的也是数据包。过程比较复杂,【我的个人理解是】我们可以把UDP的Socket想象为一个具体的宅邸,在创建的时候肯定有一个实实在在的地址,而具体要往哪里发东西,我们写在具体的包裹上面就可以了,而接收到的那一方只要拿到这个包裹就行,不用指定这个包裹必须来自哪个地方,因此接收方的Packet构造函数里没有地址信息,而有一系列的方法能查看包裹,包括从哪里寄出的,因此Packet就是包裹,Socket就是宅邸,而每个宅邸都需要有发送和接收的功能,发送和接收的都是包裹Packet。

玩Socket主要记住流程,代码查文档就可以,因为步骤相对繁琐。

 

小练习

需求1:通过UDP传输方式,将一段文字数据发送出去。

思路:

1.      建立UDPSocket服务。通过DatagramSocket对象。

2.      提供数据(字节数组),并将数据封装到数据包中。

3.      通过Socket服务的发送send功能,将已有数据包发出去。

4.      关闭资源。(因为发送数据肯定在使用系统底层资源,至少在走网卡)

会抛一堆异常:.net包的SocketException,UnknownHostException以及io包的IOException,懒得处理了,直接抛个Exception。

 

需求2:定义一个应用程序,用于接收udp协议传输的数据并处理的。

思路:

1.      定义updsocket接收服务,建立端点。接收端通常会指定一个监听的端口(这个端口要与发送端数据包里面封装的目标端口一致)其实就是给这个接收网络应用程序定义数字标识。(不定义的话,系统会随机分配)方便于明确哪些数据过来该应用程序可以处理。

2.      定义一个数据包(通过把一个空的字节数组用包的构造函数封装到包里),这个包里没有数据,用于存储接收到的字节数据。而数据包对象中有更多功能可以提取字节数据中的不同数据信息。(包收到字节数据后那个数组里面也会有数据,既可以通过包的方法取出数据,也可以用数组直接取出。)

3.      通过socket服务的receive方法将收到的数据存入已定义好的数据包中。

4.      通过数据包对象的特有功能,将这些不同的数据取出,打印在控制台上。

5.      关闭资源。

运行程序的时候可以用两个控制台分别开启。一个发送一个接受。

 

另外需要注意以下几点:

接收端有一个阻塞式方法DatagramSocket的receive方法。运用到线程的机制:没数据就等着wait,当有数据发过来就notify。

 

我们如果不指定发送端的端口的话系统也会随机分配,并且每次运行程序端口都自动加1,那是因为我们每次运行完程序,上次使用完的端口还没有被释放,还存在内存当中,所以再运行程序,默认端口就会向下顺延。所以一般发送端,也会指定一个自己程序发送的端口,多次发送都会走同一个端口。

 

【注意】数据包在构造的时候需要我们填上目的地的IP和端口,但同时也会自动添加发送它的Socket的端口以及发送主机的IP,相当于填写发件人地址,不用我们操作。

而包里的一些方法如getPort()和getAddress(),返回的都是彼端的端口和IP地址,也就说发送用的包返回的是接收端的端口和IP,接收到的包返回的是发送端的端口和IP。

包里面的信息可以用getData等方法多次获取,因为包packet里面的数据依然存在不会因为被获取了或者被Socket发走了就消失,即使Socket关闭了也可以获取。

在构造接收端的包时,构造函数new DatagramPacket(byte[],int)后面这个int参数表示的是允许接受对面传过来多长的内容。后面这个参数不能长于前面数组的长度否则运行时报异常。

 

【个人理解】在接收端用包接收到发送端的数据后,查看包的getData()方法获取缓冲区数组,再用.length查看缓冲区长度,这长度是接收端包的数组长度决定的,不管源头发过来的包是什么大小的,接收过来统一变成我们这边的包的尺寸,可以理解为把发送端小篮子里的东西放到接收端的大篮子里,就算发送端的篮子里只放了一个鹌鹑蛋,而接收端的篮子是个卡车,这里的长度也是接收端的卡车长度。该长度为包里面第一个参数(缓冲区数组)的长度,而跟第二个参数愿意接收的数据量无关。可以理解为我开了个卡车来接站,但我只允许一公斤的量(任性),结果接到一个鹌鹑蛋。包永恒不变就是这个卡车,长度自然也就是数组长度。

而包的另一个方法getLength()与上面的结果不一样,这里是实际接收到的数据的长度,就是一个鹌鹑蛋的长度。

代码示例如下:(正常开发应该把发送和接收写到两个.java文件里。这里是为了便于演示,在运行的时候要开启两个控制台,一个发送一个接收,接收先开启)

import java.net.*;

class UDPSend
{
	public static void main(String[] args) throws Exception
	{
		//1.	建立UDPSocket服务。通过DatagramSocket对象。
		DatagramSocket ds = new DatagramSocket(20000);//也可以在构造函数里指定端口,否则系统自动分配默认端口

		//2.	提供数据,并将数据封装到数据包中。
		byte[] pack = "UDP,哥们儿来了!".getBytes();
		DatagramPacket dp = new DatagramPacket(pack,pack.length,InetAddress.getByName("localhost"),10000);
		//3.	通过Socket服务的发送send功能,将已有数据包发出去。
		ds.send(dp);
		System.out.println("PORT:"+dp.getPort());
		System.out.println("IP:"+dp.getAddress().getHostAddress());
		String text = new String(dp.getData(),0,dp.getLength());
		System.out.println(text);//发送完了数据还在
		//4.	关闭资源。(因为发送数据肯定在使用系统底层资源,至少在走网卡)
		ds.close();
		text = new String(dp.getData(),0,dp.getLength());
		System.out.println(text);//即使Socket关闭了packet里面的数据还依然存在。
	}
}

class UDPRcv
{
	public static void main(String[] args) throws Exception
	{
		//1.	创建UDP Socket,建立端点。
		DatagramSocket ds = new DatagramSocket(10000);
		//2.	定义数据包。用于存储数据。
		byte[] buf = new byte[1024];
		DatagramPacket dp = new DatagramPacket(buf,buf.length);//后面这个int参数表示的是允许接受对面传过来多长的内容。后面这个参数不能长于上面定义的1024否则运行时报异常。
		//3.	通过Socket服务的receive方法将收到的数据存入数据包中。
		while (true)//设计一直让其处于监听状态,来保持运行。因为这个循环不会结束,所以要让底下的代码注释掉否则编译不通过。
		{
			ds.receive(dp);//该方法为阻塞式方法。
			//4.	通过数据包的方法获取其中的数据。
			String ip = dp.getAddress().getHostAddress();
			int port = dp.getPort();
			String text = new String(dp.getData(),0,dp.getLength());
			System.out.println("IP:"+ip+"\r\n"+"PORT:"+port+"\r\n"+"TEXT:"+text);
		}
		//5.	关闭资源。
		//ds.close();
	}
}

在例子中我们给receive加了个while(true)循环来保持监听持续运行,这个不会导致死机,因为receive是阻塞式方法,当没有读到数据的时候线程就wait了,不占资源。但是有些问题要【注意】不要把建立Socket的方法放在循环里面,第二次new的时候会产生一个新的服务,它的端口也是10000会产生.net.BindException: Address already in use: Cannot bint端口绑定异常,相当于两个应用程序共用一个端口。

但是循环里面新建Packet没问题,因为一个端口可以分配多个包,腾讯的应用端口都是一样的,但是我们会收到视频音频图片文字等各种各样封装在包里的信息,都走同一个端口,这点与Socket不一样,Socket是服务,需要独占一个端口这样才能一一对应,而包不需要,它仅仅使用端口。

程序结束后端口可能还会存在很长一段时间,所以最好把每个程序端口定义成不一样的。

 

练习:

基于上面的例子做一个小型聊天程序,可以发送和接收数据。

思想:其实就是把上面的发送接收的服务与IO技术结合起来。

代码如下:

import java.net.*;
import java.io.*;
class UDPSend2
{
	public static void main(String[] args) throws Exception
	{
		DatagramSocket ds = new DatagramSocket(20001);
		BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
		String line = null;
		while ((line=bufr.readLine())!=null)//read也是阻塞式方法
		{
			if (line.equals("886"))
			{
				byte[] buf = "对方已下线".getBytes();
				DatagramPacket dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("255.255.255.255"),10001);
				ds.send(dp);
				break;
			}
			byte[] buf = line.getBytes();
			DatagramPacket dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("255.255.255.255"),10001);//这个是广播地址,联网的时候再试试,与本机对话用:InetAddress.getLocalHost()

			ds.send(dp);
		}
		ds.close();
		bufr.close();
	}
}
class UDPRcv2
{
	public static void main(String[] args) throws Exception
	{
		DatagramSocket ds = new DatagramSocket(10001);
		while (true)
		{
			byte[] buf = new byte[1024*64];//*64表示最大64kb容量
			DatagramPacket dp = new DatagramPacket(buf,buf.length);
			ds.receive(dp);
			String ip = dp.getAddress().getHostAddress();
			String text = new String(dp.getData(),0,dp.getLength());
			System.out.println("IP:"+ip+":-------:"+text);
		}
	}	
}

发现数据的发送和接收是独立开来的,即发送的只能发送,不能接收,接收的只能接收,不能发送,需要开启两个控制台实现,很不方便。

于是想到让收数据的部分和发数据的部分同时执行。那就需要用到多线程技术。一个线程控制收,一个线程控制发。这样就能在一个控制台实现了。

因为收和发动作是不一致的,所以要定义两个run方法。而且这两个方法要封装在不同的类中。

代码详见:

import java.net.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
class UDPSend3 implements Runnable
{
	private DatagramSocket ds;
	public UDPSend3(DatagramSocket ds)
	{
		this.ds = ds;
	}
	public void run()
	{
		try
		{
			BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
			String line = null;
			while ((line=bufr.readLine())!=null)
			{
				if (line.equals("886"))
				{
					byte[] buf = "对方已下线".getBytes();
					DatagramPacket dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("255.255.255.255"),10002);
					ds.send(dp);
					break;
				}
				byte[] buf = line.getBytes();
				DatagramPacket dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("255.255.255.255"),10002);
				ds.send(dp);
			}
			bufr.close();
			ds.close();
		}
		catch (Exception e)
		{
			throw new RuntimeException("发送端失败");
		}
	}
}
class UDPRcv3 implements Runnable
{
	private DatagramSocket ds;
	public UDPRcv3(DatagramSocket ds)
	{
		this.ds = ds;
	}
	public void run()
	{
		try
		{
			while (true)
			{
				byte[] buf = new byte[1024*64];
				DatagramPacket dp = new DatagramPacket(buf,buf.length);
				ds.receive(dp);
				String ip = dp.getAddress().getHostAddress();
				String text = new String(dp.getData(),0,dp.getLength());
				System.out.println("IP:"+ip+"\r\n"+"Content:"+text+"\r\n");
			}
		}
		catch (Exception e)
		{
			throw new RuntimeException("接收端失败");
		}
	}
}
class ChatDemo
{
	public static void main(String[] args) throws Exception
	{
		DatagramSocket send = new DatagramSocket(20002);
		DatagramSocket receive = new DatagramSocket(10002);
		new Thread(new UDPSend3(send)).start();
		new Thread(new UDPRcv3(receive)).start();
	}
}


这个例子也是基于控制台的,测试发现手写区域和展示区域都集中在一块了,会出现当用户A打字打到一半的时候,用户B的消息直接来了,这样就会把输入打断。因此可以设计图形化界面把两个部分分开,那样就像QQ一样了。

代码详见:

import java.net.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
class FeiQ
{
	private Frame f;
	private TextArea taShow, taType;
	private Button but;
	private static PipedOutputStream pos;
	private static PipedInputStream pis;
	public static void main(String[] args) throws Exception
	{
		FeiQ fq = new FeiQ();
	}
	FeiQ() throws Exception
	{
		pos = new PipedOutputStream();
		pis = new PipedInputStream();
		pos.connect(pis);

		DatagramSocket send = new DatagramSocket(20002);
		DatagramSocket receive = new DatagramSocket(10002);
		UDPSend3 s3 = this.new UDPSend3(send,pis);
		UDPRcv3 r3 = this.new UDPRcv3(receive);
		new Thread(s3).start();
		new Thread(r3).start();
		init();
	}
	public void init() throws Exception
	{
		f = new Frame("飞秋");
		f.setLayout(new FlowLayout());
		f.setBounds(300,100,450,420);
		taShow = new TextArea(15,50);
		taType = new TextArea(5,50);
		but = new Button("我要发送喽");
		f.add(taShow);
		f.add(taType);
		f.add(but);
		myEvent();
		f.setVisible(true);
	}
	public void myEvent() throws Exception
	{
		f.addWindowListener(new WindowAdapter()
		{
			public void windowClosing(WindowEvent e)
			{
				try
				{
					pos.write("对方已下线\r\n".getBytes());
					Thread.sleep(1000);
					System.exit(0);
				}
				catch (Exception ex)
				{
					throw new RuntimeException("按钮按下出错了");
				}
				
			}
		});
		but.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				try
				{
					if (taType.getText().length()!=0)
					{
						sendMSG();//按钮被敲会发生的动作。
					}
				}
				catch (Exception ex)
				{
					throw new RuntimeException("按钮按下出错了");
				}
			}
		});
		taType.addKeyListener(new KeyAdapter()
		{
			public void keyPressed(KeyEvent k)
			{
				if (k.isControlDown() && k.getKeyCode()==KeyEvent.VK_ENTER)//用O作为标识符,本来应该用ENTER
				{
					k.consume();
					if (taType.getText().length()!=0)
					{
						try
						{
							sendMSG();//按钮被敲会发生的动作。
						}
						catch (Exception ex)
						{
							throw new RuntimeException("按钮按下出错了");
						}
					}
				}
			}
		});
		taShow.addKeyListener(new KeyAdapter()
		{
			public void keyPressed(KeyEvent k)
			{
				k.consume();
			}
		});
	}
	public void sendMSG() throws Exception
	{
		String s = taType.getText()+"\r\n";//变成PrintStream能优化一些
		taShow.append("俺说:"+s+"\r\n");
		taType.setText("");
		pos.write(s.getBytes());
	}
	class UDPSend3 implements Runnable
	{
		private DatagramSocket ds;
		private PipedInputStream pis;
		public UDPSend3(DatagramSocket ds, PipedInputStream pis)
		{
			this.ds = ds;
			this.pis = pis;
		}
		public void run()
		{
			try
			{
				BufferedInputStream bis = new BufferedInputStream(pis);
				byte[] buf = new byte[1024];
				int len;
				while ((len=bis.read(buf))!=-1) //意思是用数组装着传过来的数据,而不是对方传过来一个长度有限的数组,因此对方传过来的文本框里的数据不会有结尾的,相当于while(true)。
				{
					DatagramPacket dp = new DatagramPacket(buf,len,InetAddress.getByName("192.168.1.105"),10002);
					ds.send(dp);
				}
				bis.close();
				ds.close();
			}
			catch (Exception e)
			{
				throw new RuntimeException("发送端失败");
			}
		}
	}
	class UDPRcv3 implements Runnable
	{
		private DatagramSocket ds;
		public UDPRcv3(DatagramSocket ds)
		{
			this.ds = ds;
		}
		public void run()
		{
			try
			{
				while (true)
				{
					byte[] buf = new byte[1024*64];
					DatagramPacket dp = new DatagramPacket(buf,buf.length);
					ds.receive(dp);
					String ip = dp.getAddress().getHostAddress();
					String text = new String(dp.getData(),0,dp.getLength());

					taShow.append(ip+":"+text+"\r\n");
					//System.out.println("IP:"+ip+"\r\n"+"Content:"+text+"\r\n");
				}
			}
			catch (Exception e)
			{
				throw new RuntimeException("接收端失败");
			}
		}
	}
}

该示例集合了多线程、IO流、网络、图形用户界面等知识。该例子比较困难,本人才疏学浅,不甚精通,刚开始尝试了十几次都没成功。


TCP

TCP的两个Socket:Socket(客户端)和ServerSocket(服务端),两个端点建立完才能通信。

客户端:

通过查阅socket对象,发现在该对象建立时,就可以在构造函数里(Socket(InetAddress address, int port),或者更简单一些的Socket(String host, int port))去链接指定服务端主机。这与UDP不一样,后者在构造的时候设定的是本地的地址信息,在具体发送的包裹里面设定目的地信息,而TCP的客户端根本不定义自己的地址信息,因为客户端和服务端共享同样的流对象,服务端只要操作那个流对象就能给客户端返回数据。而服务端必须明确自己的地址信息,也就是端口,这样才能与客户端连接成功,才能通信。(当然Socket也有空参数的构造函数(Socket()),new完以后没有任何连接,但可以通过void connect(SocketAddress endpoint)方法去连接指定服务端)

SocketAddress与InetAddress的区别:

后者封装的是IP地址,SocketAddress(本身是抽象类)的子类InetSocketAddress封装的是IP地址+端口。

 

因为TCP是面向连接的,所以在建立socket服务时,就要有服务端存在,并连接成功。

形成通路后,在该通道进行数据的传输。

对于客户端:

一旦连接成功就说明通路建立成功了,就会产生一个Socket流(网络流),只需要通过InputStream getInputStream()和OutputStreamgetOutputStream()方法拿到Socket流里面的输入和输出流来操作流对象(不需要我们再new输入输出的流对象,内部都已经封装好)。从而完成对数据的操作。

对于服务端:

当客户端向服务端发送请求,连接成功后,服务端就会拿到客户端的Socket对象(服务端自己不会有流对象),而是通过Socket类里面的方法获取到客户端的输入输出流对象与客户端通信。

这样的另一个好处是,服务端能同时处理多个客户端的连接而不会发生混乱,输入输出流和它所属的Socket(代表了客户端)是一一对应的。

需求:

建立一个客户端一个服务端,让服务端能接收到客户端的数据并打印。

客户端步骤:

1.      创建Socket服务,并指定要连接的主机和端口。

2.      客户端使用结束后,要释放资源,关闭socket就可以了,不用关闭我们调用的流,因为流是依靠Socket而存在的。

服务端步骤:

1.      建立服务端的套接字服务,ServerSocket并监听一个端口。(构造函数ServerSocket(intport)或者设置空参数的构造函数就会默认绑定,或者后期通过绑定方法绑定端口:void bind(SocketAddress endpoint))

2.      获取连接过来的客户端对象。

通过ServerSocket的Socket accept方法。必须等客户端连接成功了通路建成了,服务端才可以接收到,而如果没有连接就会等,所以这个方法是阻塞式的。

3.      客户端如果发过来数据,服务端要使用对应的客户端对象,并获取到该客户端对象的读取流来读取发过来的数据,并打印在控制台。

4.      服务端接受完客户端的数据交互之后,一般会关闭闲置的客户端(比如说超过设定时间),因为系统资源有限。但是一般不会把服务端关闭,否则的话相当于断开网络了,别的TCP客户端请求再也无法连通了,我们看新浪搜狐,也不是说看完一条新闻整个网站就down了吧?真实项目中服务器都是7*24小时开启的,不用关。

 

代码如下:

import java.io.*;
import java.net.*;
class TcpClient
{
	public static void main(String[] args) throws Exception
	{
		Socket s = new Socket("127.0.0.1",10010);
		OutputStream out = s.getOutputStream();
		out.write("TCP 哥们儿来了".getBytes());
		s.close();//客户端使用结束后,要释放资源,关闭socket就可以了,不用关闭我们调用的流,因为流是依靠Socket而存在的。
	}
}
class TcpServer
{
	public static void main(String[] args) throws Exception
	{
		//1
		ServerSocket ss = new ServerSocket(10010);
		//2
		Socket s = ss.accept();
		String ip = s.getInetAddress().getHostAddress();
		System.out.println("IP:"+ip+".....Connected");
		//3
		InputStream is = s.getInputStream();
		byte [] buf = new byte[1024];
		int len = is.read(buf);
		System.out.println(new String(buf,0,len));
		s.close();//关闭客户端
		ss.close();//4
	}
}

运行的时候服务端要先启动,否则客户端先启动服务端没开启无法连接成功,数据发不过去。

ServerSocket还有一个构造函数:ServerSocket(intport, int backlog) backlog表示的是能连接到服务器的客户端的最大同时个数。当第backlog+1个人访问时连接失败(在客户端)

 

 

需求:建立一个文本转换服务器。

客户端给服务端发送文本,服务端会将文本转成大写再返回给客户端。

而且客户端可以不断地进行文本转换,当客户端输入over时,转换结束。

 

分析:

客户端:

既然是操作设备上的数据,那么就可以使用io技术,并按照io的操作规律来思考。

源:键盘录入。

目的:网络设备,网络输出流。

而且操作的是文本数据,可以选择字符流。

步骤:

1.      建立服务。

2.      获取键盘录入。

3.      将数据发给服务端。

4.      获取服务端返回的大写数据。

5.      结束,关闭资源。

都是文本数据,可以使用字符流操作,同时为了提高效率,加入缓冲。

 

服务端:

源:socket读取流。

目的:socket输出流。

都是文本,装饰。

 

代码详见:

import java.io.*;
import java.net.*;
class TransClient
{
	public static void main(String[] args) throws Exception
	{
		//定义读取键盘数据的流对象
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		Socket s = new Socket("127.0.0.1",10012);
		//定义目的地,将数据写入到socket输出流,发给服务端。
//		BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
		PrintWriter out = new PrintWriter(s.getOutputStream(),true);//用PrintWriter可以简化代码,因为它既可以接收字符流又可以接收字节流,同时还能自动刷新。
		//定义一个socket读取流,读取服务端返回的大写信息。
		BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
		String line = null;
		while ((line = br.readLine())!=null)
		{
			out.println(line);
//			bufOut.write(line);
//			bufOut.newLine();
//			bufOut.flush();
			String str = bufIn.readLine();
			System.out.println("Server:"+str);
			if (line.equals("Over"))
				break;
		}
		br.close();
		s.close();
	}
}
class TransServer
{
	public static void main(String[] args) throws Exception
	{
		ServerSocket ss = new ServerSocket(10012);
		Socket s = ss.accept();

		String ip = s.getInetAddress().getHostAddress();
		System.out.println("IP:"+ip+".....Connected");
		//读取socket读取流中的数据。
		BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
		//定义目的地,socket输出流,将大写数据写入到socket输出流,并发送给客户端。
		BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
		PrintWriter out = new PrintWriter(bufOut,true);

		String line = null;
		while ((line = bufIn.readLine())!=null)//readLine()方法会等结束标记,也就是换行符,如果输入的内容没有回车,就会一直等,所以一定要在写的时候加上换行,并且因为是字符流的输出,所以要刷新缓冲区,否则就僵局了。
		{
			/*if (line.equals("Over"))//客户端的Socket流在close的时候会自动通过输出流发出一个-1的标记给服务器端。所以服务端的read方法会读到结束标记(注意不是换行标记)跳出循环,所以这两行判断是否为跳出关键字的代码不写服务端也会结束。
				break;*/
			out.println(line.toUpperCase());
//			bufOut.write(line.toUpperCase());
//			bufOut.newLine();
//			bufOut.flush();
		}
		s.close();
		ss.close();
	}
}

该例子出现的问题是客户端和服务端都在莫名地等待。

原因是客户端和服务端都有阻塞式方法,这些方法没有读到换行标记或者结束标记,就会一直等,而导致两端都在等待。

开发时如果再出现这种现象,就查阻塞式方法

该例代码的另外两点补充:

1.      客户端的Socket流在close的时候会自动通过输出流发出一个-1的标记给服务器端。所以服务端的read方法会读到结束标记(注意不是换行标记)跳出循环,所以这两行判断是否为跳出关键字的代码不写服务端也会结束。

2.      PrintWriter可以简化代码,因为它既可以接收字符流又可以接收字节流,同时还能自动刷新。

 

我们通过实际操作会发现,这个服务端有个局限性,当A客户端连接上以后。被服务端获取到,服务端执行具体流程。

这时B客户端连接,只有等待。因为服务端还没有处理完A客户端的请求,还没有循环回来执行下一次accept方法,所以暂时获取不到B客户端对象。

那么为了可以让多个客户端同时并发访问服务端,那么服务端最好将每个客户端封装到一个单独的线程中,这样就可以同时处理多个客户端请求。(这就是所有服务器运行的根本原理,否则无法处理并发数据

如何定义线程:

只要明确了每一个客户端要在服务端执行的代码即可,将该代码存入run方法中。

客户端并发上传图片,代码如下:

import java.io.*;
import java.net.*;
class PicClient
{
	public static void main(String[] args) throws Exception
	{
		if (args.length!=1)
		{
			System.out.println("一次接受一个文件");
			return;
		}
		File file = new File(args[0]);//需要在命令行里输入java PicClient D:\树蛙.jpg才能把文件名传入。
		if (!(file.exists() && file.isFile()))
		{
			System.out.println("该文件不存在");
			return;
		}
		if (!(file.getName().endsWith(".jpg")))
		{
			System.out.println("图片格式错误,只接受jpg");
			return;
		}
		if (file.length()>1024*1024*10)
		{
			System.out.println("容量超过限制,最大接受10MB");
			return;
		}
		//上面是加入的一些判断,防止服务器过载。
		Socket s = new Socket("127.0.0.1",10020);
		FileInputStream fis = new FileInputStream(file);
		OutputStream out = s.getOutputStream();
		byte[] buf = new byte[1024];
		int len;
		while ((len=fis.read(buf))!=-1)
		{
			out.write(buf,0,len);
		}
		//告诉服务端数据已写完
		s.shutdownOutput();

		InputStream in = s.getInputStream();
		byte[] bufIn = new byte[1024];
		int lenIn = in.read(bufIn);
		System.out.println(new String(bufIn,0,lenIn));

		fis.close();
		s.close();
	}
}
class PicThread implements Runnable
{
	private Socket s;
	int count = 1;
	PicThread(Socket s)
	{
		this.s = s;
	}
	public void run()
	{
		String ip = s.getInetAddress().getHostAddress();
		System.out.println("IP:"+ip);
		try
		{
			InputStream in = s.getInputStream();
			File file = new File(ip+".jpg");
			while (file.exists())
				file = new File(ip+"("+(count++)+")"+".jpg");
			//上述代码为防止重名。
			FileOutputStream fos = new FileOutputStream(file);
			byte[] buf = new byte[1024];
			int len;
			while ((len=in.read(buf))!=-1)
			{
				fos.write(buf,0,len);
			}
			OutputStream out = s.getOutputStream();
			out.write("上传成功".getBytes());
			fos.close();
			s.close();
		}
		catch (Exception e)
		{
			throw new RuntimeException(ip+"...写入异常");
		}
		
	}
}
class PicServer
{
	public static void main(String[] args) throws Exception
	{
		ServerSocket ss = new ServerSocket(10020);
		while (true)
		{
			Socket s = ss.accept();
			new Thread(new PicThread(s)).start();
		}
		//ss.close();
	}
}

上例通过主函数传值的形式 args,把需要上传的文件告诉给程序。并加入的一些判断,防止服务器过载,提升代码健壮性。而且为防止重名,加入了一些判断和循环语句。

多线程需要多台机器同时往一个服务器传图片才能看出效果。而传输时间往往较短,所以可以通过在代码里加入让线程等待的语句让结果更明显一些。

 

 

练习:可以通过浏览器访问的服务端

演示客户端和服务端。

1.      客户端:浏览器

2.      服务端:自定义

客户端还可以是telnet(windows提供的远程登陆的工具,可以连接网络中任意一台主机,在DOS命令行下连接,连接之后可以对这台主机进行命令式的配置)可以理解为一个客户端软件。

用法是: telnet 127.0.0.1 10000(命令 ip 端口)

但是win7里这个软件貌似没了。

代码详见:

import java.io.*;
import java.net.*;
class ServerDemo
{
	public static void main(String[] args) throws Exception
	{
		ServerSocket ss = new ServerSocket(10022);
		while(true)
		{
			Socket s = ss.accept();
			new Thread(new ServerThread(s)).start();
		}
	}
}
class ServerThread implements Runnable
{
	private Socket s;
	ServerThread(Socket s)
	{
		this.s = s;
	}
	public void run()
	{
		String ip = s.getInetAddress().getHostAddress();
		System.out.println("IP:"+ip);
		try
		{
			InputStream in = s.getInputStream();
			byte[] buf = new byte[1024];
			int len;
			while ((len=in.read(buf))!=-1)
			{
				System.out.println(new String(buf,0,len));
			}

			PrintWriter pwOut = new PrintWriter(s.getOutputStream(),true);
			pwOut.println("<font color='red' size='7'>客户端你好</font>");

			
			s.close();
		}
		catch (Exception e)
		{
			throw new RuntimeException(ip+":异常");
		}
	}
}

演示这个代码需要我们打开控制台,启动Server服务,然后打开浏览器,在地址栏里输入http://127.0.0.1:10022/表示本地回环地址下的10022端口,也就是服务器所在的端口。正常情况下会看到控制台上显示的是浏览器默认输入过来的一些信息。而浏览器上显示的是我们服务器自定义的消息。

客户端:浏览器

服务端:Tomcat服务器。

Tomcat服务器其实就是建立了一个服务端,能处理浏览器的交互数据请求,并把存储在自己文件夹里的网站数据处理以后发送给浏览器端,相当于模拟了真实网站的运行。

 

练习:自己做一个浏览器

需求:想设计一个自定义的客户端,不用浏览器。服务端还是Tomcat服务器,获取服务器的数据。

那么我们必须先知道浏览器和服务端在交互的时候浏览器都发送了什么数据,可以通过我们自己设计的服务端的应用程序打印一下。

得到HTTP的请求消息头如下(对应的有应答消息头):

GET / HTTP/1.1

Host: 127.0.0.1:10022

Connection: keep-alive

Cache-Control: no-cache

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Pragma: no-cache

User-Agent: Mozilla/5.0 (Windows NT 6.1)AppleWebKit/537.36 (KHTML, like Gecko)

Maxthon/4.4.3.2000 Chrome/30.0.1599.101Safari/537.36

DNT: 1

Accept-Encoding: gzip,deflate

Accept-Language:zh-CN         

浏览器和Tomcat服务器在底层都走的TCP协议自不用说。但在应用层都走的HTTP协议

目前HTTP协议就两个版本1.0和1.1,多用新版

第一行完全体应该这样:GET/myweb/demo.html HTTP/1.1 中间要填上具体的路径。这一行内容是最关键的,服务端得知道你要访问哪个部分。

Accept:表示支持的应用或者插件,比如说flash,迅雷,在线视频等,不支持服务器就不会发数据给浏览器,因为发了浏览器也无法解读。*/*表示除了上述以外其他也都支持。

Accept-Encoding:表示支持的压缩方式,服务端为了节约流量,多会把自己的数据打包完了再发给客户端,客户端浏览器再解压缩,所以需要明确哪种压缩方式。

Connection: keep-alive表示数据发送完后是否断开连接,如果选择closed数据发送完了就会断开。

Host:表示要访问哪台主机的哪个端口。一个服务器可以有多台主机,多个网卡。

最后一行后面有一空行,空行下面是请求数据体。空行是分隔请求消息头和请求数据体的标志(HTTP协议规定的)

下一步就是做浏览器并访问Tomcat:

新建一个客户端Socket,将上述HTTP规定的信息通过Socket输出流发给Tomcat服务器,再用Socket的读取流看看服务器返回什么,发现Tomcat服务器会返回下列信息。


这就是HTTP应答消息头

第一行200是响应状态码,代表成功,请求的数据有,而且给客户端回馈了。OK是它的描述信息。

自定义的浏览器代码如下:

import java.io.*;
import java.net.*;
class MyIE//最好有Tomcat服务器测试看看返回的标准HTTP应答消息头是什么样的。
{
	public static void main(String[] args) throws Exception
	{
		Socket s = new Socket("127.0.0.1",10022);
		PrintWriter pwOut = new PrintWriter(s.getOutputStream(),true);
		InputStream in = s.getInputStream();
		pwOut.println("GET /myweb/demo.html HTTP/1.1");
		pwOut.println("Accept: */*");//表示接受所有类型
		pwOut.println("Accept-Language: zh-CN	");
		pwOut.println("Host: 127.0.0.1:10022");
		pwOut.println("Connection: closed");//表示数据发送完后是否断开连接,closed表示断。
		pwOut.println();
		pwOut.println();//加入空行,空行是分隔请求消息头和请求数据体的标志(HTTP协议规定的),这里为了保险加入两行

		BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
		String line = null;
		while ((line=bufIn.readLine())!=null)//如果上面的Connection不设为closed的话这里就会阻塞在readLine方法上,会一直等服务端发过来的新数据。如果设为closed服务端就会把网络流关掉,从而这里能读到null结束循环。
		{
			System.out.println(line);
		}
		s.close();
	}
}

上例中的代码仅仅是一个简单的DOS界面下的浏览器,一夜回到80年代。。。那如果想做一个现代的浏览器我们肯定就需要图形化界面。同时,地址栏要能接收用户输入的参数,不能再是我们在代码里手动指定的了,那样用户肯定用不了。

而地址栏输入的地址相对复杂,比如说一个完整的URL如下面这个:

http://192.168.1.254:8080/myweb/demo.html?name=haha&age=30

有ip地址,有端口,有访问文件的具体路径,比较麻烦。而我们要自定义浏览器的话肯定需要对地址进行分析就需要我们把用户输入的一串字符串地址进行切割拆分。那么Java对这类很常见的复杂事物肯定会封装一个专门的对象。那就是URL类。

补充小知识:Uniform Resource Indentifiers(URI)URI的范围比URL大,条形码也属于URI的范畴。

URL类的构造函数可以传一个字符串URL进去,也可以分别传ip,端口,路径等等。

创建URL对象会产生一个MalformedURLException异常(如果URL格式不正确,肯定解析不了这个地址)

常见方法:

String getFile()

          获取此 URL 的文件名。

 String getHost()

         获取此 URL 的主机名(如果适用)。

 String getPath()

         获取此 URL 的路径部分。

 intgetPort()

         获取此 URL 的端口号。

 String getProtocol()

         获取此 URL 的协议名称。

 String getQuery()

         获取此 URL 的查询部分。

getQuery获取的就是地址栏里面的参数信息(有些时候往服务端提交需要带着客户端的一些特定信息,那么这时就用加参数的形式把这些信息提交到服务端去),如name=haha&age=30。它与前面的路径用?分隔开。并以键值对的形式存在,用&连接几个键值对。表单提交用的就是这个。getFile也会带着这个参数,它相当于就是把getPath和getQuery合在了一起。

比如说我们再看看上面的示例URL

http://192.168.1.254:8080/myweb/demo.html?name=haha&age=30

getPath():/myweb/demo.html

getFile():/myweb/demo.html?name=haha&age=30

getQuery():name=haha&age=30

如果在地址栏里没有写端口号的话,服务端读到的端口号就是-1,一般的服务端会做一个判断,如果端口号是-1就给分配一个默认端口,比如说80.

URL里的URLConnection openConnection() 方法会返回一个 URLConnection对象,它表示到 URL 所引用的远程对象的连接。

URLConnection是一个抽象类,它有两个子类。HttpURLConnection, JarURLConnection。

每次这个方法一使用就会打开一个新的连接,就相当于打开了客户端和服务端的通路(已经把做连接的动作,也就是Socket封装到内部)。而且它还是带着协议封装的,会自动把相应协议装包之后通过传输层发送过去。如HTTP,所以用了这个办法就不用再new Socket了(传输层),也不用像在传输层那样单独发送请求消息头,因为此方法直接在应用层建立连接。它也有InputStream getInputStream()  和OutputStreamgetOutputStream()  方法(其实内部走的也是socket只不过封装好了)可以获取到流对象。而且此连接不用关。

用URLConnection的方法获取到的数据流,既不用给服务端发送请求消息头,读取到服务端的返回数据也没有应答消息头。这是因为通过传输层过来的数据要层层往上传递并被处理解析(把识别的数据解开),而URLConnection是应用层的,它会把数据包中的头部分信息(应用层能识别)给拆包,解开了,并把有效的主体部分数据向上传递。发送出去的数据也一样,我们只要定义好主体数据就可以了,URLConnection会自动给我们加上相应的请求消息头。

当然如果我们非要获取到头部的内容,也可以通过URLConnection的String getContentType()等方法。

 

 

在浏览器写入一条URL去访问某一台主机中的某个数据时,具体做了什么事情:(必须掌握)

首先阅读协议,然后根据协议启动相应引擎解析后面的内容,并把主机和端口封装成socket。

有的时候主机名不是IP地址,而是一串人类字符,也就是域名,如www.sina.com,所以必须把它翻译成IP地址才能去访问这台主机。想要将主机名翻译成IP地址,需要域名解析(DNS域名解析服务器)。这个浏览器会先去公网上找一台域名解析服务器(如果没有指定的话会自动搜索提供网络服务的运营商的就近的DNS服务器),该服务器上记录的是每一个域名对应的IP地址(映射关系表)。然后再拿着得到的新浪的IP地址找到真正新浪的主机。

其实本地也有一个IP地址与域名的配置文件,就在C:\Windows\System32\drivers\etc\hosts里面定义了。里面把localhost这个域名与本机回环地址127.0.0.1绑定了,一个IP地址可以对应多个域名。如果我们把www.sina.com也绑定到127.0.0.1这个地址上会发现在浏览器里输入www.sina.com:8080时访问的其实是本地的Tomcat服务器(如果启动了的话)。

所以,域名解析真正的顺序是:先去本地的hosts文件里面找,找不到再上公网的DNS找,不管在哪找到的,拿到指定的IP地址就开始访问。如果都没有去查到所访问的域名,或者连接超时,就会出现该页无法显示。

那么这个小知识有什么具体用途呢?

应用1:

我们可以通过给hosts增加一些常用的网站的IP地址来提高速度。

应用2:

有些收费注册软件每次启动会自动上自己的网址检查是否为合法用户。如www.adobe.com,我们可以在hosts里面把这个网址改为本机回环地址127.0.0.1这样它就找不到自己的网站从而无法识别出软件是盗版的了。

应用3:

屏蔽恶意网站,有些杀毒软件如360会自动把一些黑名单里的网站加入到hosts文件里链接到本地从而让他们无法被访问。

但如果直接输入IP地址就直接连接到指定主机而不会通过hosts或者DNS了。这样还是能访问到恶意网站。

想要拿到一个域名的IP地址可以先用InetAddress类里的InetAddress.getByName("www.baidu.com")方法拿到InetAddress对象,再用StringgetHostAddress()  方法获取该对象的IP地址。

如百度的IP地址就是61.135.169.121



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值