网络编程是非常重要的内容,在信息时代的背景下,网络已经成为人们之间交互的重要方式,我们这一章说一说java中网络编程的基础知识。
一. 网络编程的介绍
1. 网络中的信息交互
网络是用来共享数据的,我们可以通过网络来获取信息,进行数据的交换,在网络上进行信息交互必须要由3个前提来完成。
(1)IP地址
用来表征网络中的设备,他是由4段数字构成,有些难以记忆,可以用主机名也就是一个字符来代表此设备,例如在我们的IP地址段中,有一个地址是很特殊的,就是127.0.0.1。这个地址是本机地址,又叫本地回环地址,他可以用来测试网卡是否正常,他可以用localhost来替代127.0.0.1。
(2) 端口
端口是用来标识应用程序的逻辑地址,我们通过IP找到对方的主机,通过端口来找到要进行连接的应用程序,端口的取值范围有:0到65535,其中0到1024一般为系统使 用或用作保留端口,一般我们自定义端口时不知道这个范围内的。
(3)通信协议
在网络上通信必须要有一个标准,用来进行网络通信的设备必须使用统一的标准,常见的通信协议有TCP/IP协议,UDP协议等。
2.网络的划分
计算机网络在不断的发展中人们把他划分成不同层次的网络,虽然这个划分并不是特别统一,但是都能概括网络的组成,我们看一下两种网络模型的划分:OSI网络模型和TCP/IP网络模型:
这两种模型都对网络进行了划分,只不过划分的层数不一致,OSI模型把网络划分的更加细致,分为7层,TCP/IP划分了4层,但是他们传输的方式是一致的,我们的数据都是在应用层由客户指定然后自上而下经过层层封包最终在物理层也就是网线光纤等传输介质把信息发送给另一端的主机,接收端的主机经物理层接收过数据后经过层层解包最终在应用层显示出来,这个传输的过程是没有变化的。
在传输层、网络层的应用协议主要有TCP/IP协议和UDP协议,在应用层使用的协议主要有HTTP和FTP协议,自网络层向上就是软件开发所要接触的领域,所要我们必须要了解这些协议,并能使用它们。
3.IP地址类
Java中为我们提供了一个类用来描述IP地址,叫做InetAddress,这个类中没有构造方法,但他提供了方法来获取到本类实例。
1> getLocalHost()
返回InetAddress实例,获取到本机主机名和IP地址
2> getHostAddress()
返回IP地址的字符串形式
3> getHostName()
返回String类型,获取主机名
4> getByname(String host)
根据指定的主机名,获取到此主机的InetAddress对象
class InetAddressDemo1 {
public static void main(String[] args) {
try {
//获取本机InetAdress实例
InetAddress ia = InetAddress.getLocalHost();
//获取到本机IP地址
System.out.println("myIp:"+ia.getHostAddress());
//获取本机名字
System.out.println("myHostName:"+ia.getHostName());
//通过主机名获取InetAdress实例
InetAddress baidu = InetAddress.getByName("www.itheima.com");
System.out.println("heimaIP:"+baidu.getHostAddress());
System.out.println("heimaName:"+baidu.getHostName());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
/*打印:
* myIp:192.168.0.103
myHostName:wygsqsj
heimaIP:42.121.41.246
heimaName:www.itheima.com
* */
}
二. 网络通信
1. UDP和TCP协议
UDP和TCP都是应用于网络层和传输层的协议,我们来分别说一说他们的特点。
UDP协议主要有四个特点:
1. 将数据及源和目的封装成数据包,不需要建立连接
2. 每个数据包的大小限制在64k以内
3. 因为无需建立连接,是不可靠的协议
4. 使用UDP协议,传输速度非常快
当我们要传输的数据比较小,对于速度要求较高而且对连接的可靠性并没有太高要求时, 我们使用UDP协议来传输数据,比如视频会议和网络聊天。
TCP协议的特点:
1. 通过三次握手建立连接,形成传输通道。
2. 使用TCP协议可以进行大数据量的传输。
3. 通过三次握手建立连接,是可靠协议。
4. 传输的效率稍低。
当我们要保证连接的可靠性,而且是大批量数据的传输时我们就要选择TCP协议,虽然相比于UDP来说要慢,但是不会有丢包的现象。TCP协议一般应用于通话和下载。
2. Socket机制
(1)Socket是java为网络通信提供的一种机制,在java中称Socket为套接字,通过Socket就可以建立起收发端的连接。Socket机制的特点:
1> 需要通信两端都建立Socket端点
2> 数据在两个Socket之间通过IO传输
3> 网络通信时间上就是Socket通信
需要注意的是不同的协议下有不同的Socket机制,所以每种传输协议建立Socket的方式也不同。
3. UDP协议收发数据
(1)UDP传输对应着两个类,DatagramSocket类和DatagramPacket类。
DatagramSocket类用来发送与接收数据包中的套接字,DatagramPacket类用于表示数据包,数据包是通过DatagramSocket来发送或接收的。
(2)UDP协议下的Socket机制
发送数据的流程
1>建立UdpSocket服务,通过创建一个DatagramSocket对象来建立
2>提供数据,并将数据封装到数据包中。
3>通过Socket服务的发送功能,将数据包发送出去
4>关闭Socket
示例:
class UdpDemo1 {
public static void main(String[] args) {
udpSend();
}
//UDP发送数据流程
private static void udpSend() {
DatagramSocket ds = null;
//建立发送端socket服务,捕捉Socket异常
try {
ds = new DatagramSocket(65432);
//封装数据
byte[] buf = "UDP通信,我是发送端!".getBytes();
try {
//指定发送的目的,捕获未知主机异常
DatagramPacket dp = new DatagramPacket(
buf, buf.length,InetAddress.getLocalHost(),5050);
try {
//发送数据包,捕获IO异常
ds.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
} catch (SocketException e) {
e.printStackTrace();
}
//关闭资源
finally{
ds.close();
}
}
}
接收数据流程
1> 建立UdpSocket服务,注意一点要在构建DatagramSocket对象的时候为接收端指定端口号,要不然发送端发过来的数据就收不到了。
2> 定义一个数据包,用来存储接收到的数据,因为数据包对象才最明白如何操作数据包。
3> 通过Socket服务的receive方法将接收到的数据存入定义好的数据包中。
4> 通过数据包对象的特有功能提取数据,将这些数据打印在控制台上。
5> 关闭Soket
示例:
class UdpReceive {
public static void main(String[] args) {
udpreceive();
}
private static void udpreceive() {
//建立接收端Socket
DatagramSocket ds = null;
try {
//注意要指定端口号
ds = new DatagramSocket(5050);
//建立接收端的数据包用于操作接收来的数据
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, 1024);
try {
//把数据读取到定义好的数据包中
ds.receive(dp);
//利用数据包的方法获取信息
System.out.println("hostName:"+dp.getAddress().getHostName());//获取发送端主机名
System.out.println("port:"+dp.getPort());//获取端口号
System.out.println(new String(dp.getData(),0,dp.getLength()));//获取数据
} catch (IOException e) {
e.printStackTrace();
}
} catch (SocketException e) {
e.printStackTrace();
}//关闭Socket
finally{
ds.close();;
}
}
}
(3)关于UDP的小示例,我们通过多线程技术来让接收端和发送端在一个进程中执行。
//发送线程
class SendDemo implements Runnable
{
private DatagramSocket ds;
SendDemo(DatagramSocket ds)
{
this.ds = ds;
}
public void run()
{
try
{
//键盘录入数据
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String line = null;
while((line = br.readLine())!=null)
{
//输入”out“代表退出
if("out".equals(line))
break;
byte[] buf = line.getBytes();
//将buf中的内容添加到数据包中,发送到本机的20000端口
DatagramPacket dp = new DatagramPacket(buf,buf.length,
InetAddress.getByName("127.0.0.1"),20000);
ds.send(dp);
}
//如果发送完成,释放发送资源
ds.close();
}
catch (Exception e)
{
throw new RuntimeException("发送端错误");
}
}
}
//接收线程
class ReceDemo implements Runnable
{
private DatagramSocket ds;
ReceDemo(DatagramSocket ds)
{
this.ds = ds;
}
public void run()
{
try
{
while(true)
{
//定义数据包以用来存储发过来的数据
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,buf.length);
//获取数据
ds.receive(dp);
//获取ip地址
sop(dp.getAddress().getHostAddress());
//获取信息
sop(new String(dp.getData(),0,dp.getLength()));
}
}
catch (Exception e)
{
throw new RuntimeException("接收端错误");
}
}
public void sop(Object obj)
{
System.out.println(obj);
}
}
class ThreadUdp
{
public static void main(String[] args)throws Exception
{
DatagramSocket dssend = new DatagramSocket();
//接收端口号为20000
DatagramSocket dsrece = new DatagramSocket(20000);
new Thread(new SendDemo(dssend)).start();
new Thread(new ReceDemo(dsrece)).start();
System.out.println("Hello World!");
}
}
4. TCP收发数据
(1)TCP通信的简介
Socket客户端和ServerSocket服务端,这个两个类是TCP协议使用的类,我们已经了解到TCP协议是面向连接的,所以Socket和ServerSocket必须是同时存在的,要不然怎么来建立连接。
TCP通信建立好连接后,必须要通过IO技术来进行数据的传递,这个IO流是封装到了Socket中,可以通过Socket的方法getInputStream()和getOutputStream()获取到。
(2)TCP通信的步骤
①建立客户端Socket的方法是:
1> 创建Socket对象,并指定要连接的主机名和端口号
2> 获取Socket中的输出流,通过getOutputStream()方法,如果要获取服务端反馈来的数据,需要再拿到输入流,使用getInputStream ()方法。
3> 在输出流中写入数据,此数据随输出流传递到服务端
4> 关闭Socket
class Client {
public static void main(String[] args) {
try {
//创建Socket,指定主机名和端口号
Socket client = new Socket("127.0.0.1",10000);
//获取输出流
OutputStream os = client.getOutputStream();
//写出数据
os.write("你好!服务端!".getBytes());
//获取输入流,里面有服务端反馈的数据
InputStream is = client.getInputStream();
int len = 0;
byte[] buf = new byte[1024];
if((len = is.read(buf))!=-1)
{
System.out.println(new String(buf,0,len));
}
//关闭资源
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
②建立服务端ServerSocket的方法
1> 建立ServerSocket对象,指定好端口号,以便客户端能寻找到。
2> 通过accept方法来获取到客户端的对象,服务端是通过获取到客户端对象来对客户端进行操作的,因为一个服务端可能对应多个客户端,怎么来区分这些客户端,当然让他们自己操作自己来的方便,所以服务端采取获取客户端对象的方式操作客户端。
3> 通过客户端对象的getInputStream()方法获取到到客户端发送来的信息,如果需要反馈信息,通过getOutputStream()方法获取此客户端的输出流,然后将信息写入到输出流中。
4> 关闭服务端和客户端,一般服务端是不用关闭的,因为在现实中,服务端会不定时的接收客户端的请求,所以不用关闭,但是客户端连进来后完成了任务就必须得关闭,不能造成占着茅坑不拉屎的情况发生。
class Server {
public static void main(String[] args) {
try {
//创建服务端
ServerSocket ss = new ServerSocket(10000);
//获取客户端实例
Socket s1 = ss.accept();
//获取客户端读取流
InputStream is = s1.getInputStream();
int len = 0;
byte[] buf = new byte[1024];
//如果获取到内容,反馈信息给客户端
if((len = is.read(buf))!=-1)
{
//打印客户端IP地址
System.out.println(s1.getInetAddress().getHostAddress());
//打印读取到的内容
System.out.println(new String(buf,0,len));
//获取客户端写入流
OutputStream os = s1.getOutputStream();
//给客户端写入数据
os.write("你好,客户端,我收到了!".getBytes());
}
//关闭资源
s1.close();
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. TCP的应用
(1)建立一个文本转换服务器,客户端利用键盘录入文本数据,然后给服务端发送,服务端会将文本转成大写返回给客户端,,当客户端输入“out”时,转换结束。
注意当服务端接收不到客户端的数据,导致程序卡在那,这个问题出现的原因是当我们在操作文本数据通常会使用字符流来操作,只要是字符流输出就需要刷新,要不然数据没有写入到指定位置而是在缓冲区中。
在此例程中非常容易出现一个问题就是服务端和客户端都卡在那里,这是因为服务端和客户端都有阻塞式方法,如果他们没有读到结束标记,那就会等待下去。
/*
客户端:
1.建立Socket,绑定服务端的IP和端口号
2.获取键盘录入,利用读取流缓冲区
3.拿到Socket中的输出流,把键盘录入的数据写入到输出流中
4.获取服务端反馈的数据,并打印在控制台上。
5.关闭Socket
*/
class Client
{
public static void main(String[] args)throws Exception
{
//建立Socket
Socket s = new Socket("127.0.0.1",8888);
//获取键盘录入
BufferedReader brkey = new BufferedReader(
new InputStreamReader(System.in));
//获取输出流,为了简化书写,使用打印流
PrintWriter pwout = new PrintWriter(s.getOutputStream(),true);
//获取输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s.getInputStream()));
//获取键盘录入的数据
String line = null;
while((line = brkey.readLine())!=null)
{
//把获取的数据吸入输出流
pwout.println(line);
//输入“out”退出
if("out".equals(line))
break;
//获取服务端反馈的数据
System.out.println(brin.readLine());
}
//关闭资源
s.close();
brkey.close();
}
}
/*
* 服务端
* 1.建立服务端,指定端口号
* 2.获取客户端对象
* 3.获取客户端对象输入流
* 4.获取客户端输出流
* 5.通过输入流获取到客户端发过来的文字
* 6.将文本转换成大写后写入输出流
* 7.关闭资源
* */
class Server
{
public static void main(String[] args)throws Exception
{
//建立服务端
ServerSocket ss = new ServerSocket(8888);
//获取客户端
Socket s1 = ss.accept();
//获取客户端的输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s1.getInputStream()));
//获取客户端输出流
PrintWriter pw = new PrintWriter(s1.getOutputStream(),true);
//打印客户端IP
System.out.println(s1.getInetAddress().getHostAddress());
//获取客户端输入流
String line = null;
while((line = brin.readLine())!=null)
{
if("out".equals(line))
break;
System.out.println(line);
//转成大写写入到输出流
pw.println("server:"+line.toUpperCase());
}
//关闭Socket
s1.close();
ss.close();
}
}
(2)利用客户端发送一个文本文件给服务端,服务端接收后存放到硬盘中,就是一个文本文件的传输。
这个练习中,有这样一个容易出错的地方,当客户端读取完文件并把文件通过TCP协议传输给了服务端,服务端接收到了文件并成功的存储在电脑上,但是程序卡在这了,服务端停在那里,没有给客户端发送“已经完成”字样,这是因为,客户端在读取文件时,该文件是有结束标记的,他读取到结束标记就不再执行而是跳出while循环,但是有一条非常非常非常容易忽略的问题就是,我们利用read方法读数据时,当读取到结束标记然后就退出,这个结束标记是不会发送给服务端,也就是服务端能接收到文件的内容但是他接收不到结束标记,所以服务端就一直在那等着,望眼欲穿,结果程序就卡在那了。
这个时候我们有两种解决方案,
第一种:自定义结束标记,这种方法你可以自定义一个结束标记,当把文件的内容全部发送给服务端之后,再发送这个标记,当服务端读取到这个标记之后,他就明白读到最后了,要跳出了,通常我们不选择写文本作为标记,因为我们的文件中可能会有与结束标记重复的元素,这样的话会造成我们程序的不完整,所以一般都选择一个时间作为标记。具体做法是①先获取到一个时间点,把这个时间点率先发给服务端,让服务端知晓。②当文件内容全部发送给服务端后,再次把这个时间点发送给服务端③服务端读取到这个时间点跳出循环。
第二种:利用Socket为我们提供的方法。在Socket类中,考虑到了此问题的发生,所以提前给我们想出了解决方法,shutdownOutput()关闭输出流和shutdownInput()关闭输入流,这两个方法就是相当于给流中写入结束标记。
/*
客户端:
1.建立Socket
2.获取硬盘上的文本文件,利用字符流缓冲区
3.获取Socket中的输出流,把读取的文本写入到输出流中,注意发送结束信号给服务端
4.获取Socket中的输入流,接收服务端信息
5.关Socket
*/
class Client
{
public static void main(String[] args)throws Exception
{
//建立Socket
Socket s = new Socket("127.0.0.1",6666);
//获取硬盘上的一个文本文件
BufferedReader brcom = new BufferedReader(
new FileReader(new File("e://记事本.txt")));
/* //获取时间戳,先发送给服务端让他作为结束标记
long time = System.currentTimeMillis();
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeLong(time);*/
//获取客户端的输出流
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
//将硬盘上的数据写入到输出流中
String line = null;
while((line = brcom.readLine())!=null)
{
pw.println(line);
}
//当数据写完之后,调用Socket的shutdownOutput()关闭输出流,相当于写入结束标记
s.shutdownOutput();
/* pw.println(time+"");//再次写入定义的时间戳*/
//获取客户端的输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s.getInputStream()));
//获取服务端的反馈信息
System.out.println(brin.readLine());
//关闭资源
s.close();
}
}
/*
服务端
1.建立服务端
2.获取客户端实例
3.获取客户端输入流,拿到数据
4.将拿到的文本写入到本地磁盘中
5.获取客户端输出流,反馈信息给客户端已收到
6.关闭
*/
class Server
{
public static void main(String[] args)throws Exception{
//建立服务端
ServerSocket ss = new ServerSocket(6666);
//获取客户端shil
Socket s1 = ss.accept();
//获取客户端的输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s1.getInputStream()));
// //获取时间戳
// DataInputStream dis = new DataInputStream(s1.getInputStream());
// long time = dis.readLong();
//获取客户端输出流
PrintWriter pwout = new PrintWriter(s1.getOutputStream(),true);
//新建写入流,将读取到的数据写入到本地文件中
PrintWriter pwcom = new PrintWriter(
new FileWriter("d://你妹.txt"),true);
System.out.println(s1.getInetAddress().getHostAddress());
//获取Socket中的数据
String line = null;
while((line = brin.readLine())!=null)
{
//将获取到数据写入到本地文件中
pwcom.println(line);
/*//如果获取到的数据是时间戳,说明到达最后一行
if((time+"").equals(line))
{
System.out.println(time);
break;
}*/
}
//写完后,反馈给客户端
pwout.println("已经完成");
//关资源
s1.close();
ss.close();
pwcom.close();
}
}
(3)客户端的并发登陆
在TCP的连接中,如果想多个客户端想并发登陆服务端,那么服务端就不能是一条线程,因为当只有一条线程在运行时,只有一个accept方法,客户端A连接到服务端后只有等服务端处理完A的请求,循环完后才能再次执行到accept方法,获取到其他客户端,试想一下,如果服务端是这样的话,那n多客户端要想连接到服务端就太麻烦了。
所以如果客户端可以并发登陆服务端,那么服务端必定是多线程,而且开启线程的条件就是accept方法获取到了客户端,然后把每个线程都要执行的代码封装到一个线程类中,这样每个客户端都可以访问服务端并执行相应的代码了。
通过多个客户端并发上传MP3文件的示例来说明一下该应用,客户端的代码与前几个示例相似,就不再赘述,主要看看服务端的代码:
/*
* 并发上传文件,服务端
* 1.建立服务端,获取客户端对象
* 2.每获取到一个客户端对象就开启一条线程
* 3.在线程类中封装一下内容
* 首先获取到文件名字
* 获取客户端对象的输入流,读取数据
* 建立新的输出流,将Socket中的数据写入到本地磁盘
* 完成后关闭客户端对象
* */
class Server {
public static void main(String[] args)throws Exception {
//服务端一般不关闭,通过accept阻塞式方法来获取连接建立的客户端
//建立服务端
ServerSocket ss = new ServerSocket(8889);
while(true)
{
//获取客户端
Socket s = ss.accept();
//每获取到一个客户端就开启一条线程
new Thread(new PicThread(s)).start();
}
}
}
//定义一个线程类
class PicThread implements Runnable{
//接收一个客户端对象
private Socket s;
PicThread(Socket s)
{
this.s = s;
}
public void run()
{
//获取客户端ip
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"run time...");
try {
int count= 1;
//获取客户端对象的输入流
BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
//判断文件是否存在,名称参考客户端ip地址和 计数器的值,
File f = new File("e://"+ip+"("+count+")"+".txt");
while(f.exists())
f = new File("e://"+ip+"("+(count++)+")"+".txt");
//建立输出流,写入到本地磁盘中的固定文件
PrintStream ps = new PrintStream(new FileOutputStream(f));
//读取客户端中的数据,写入到指定的文件中
byte[] buf = new byte[1024];
int len = 0;
while((len = bis.read(buf))!=-1)
{
ps.write(buf, 0, len);
}
//反馈信息给服务端
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println("数据上传成功!");
//关闭资源
s.close();
ps.close();
} catch (Exception e) {
throw new RuntimeException(ip+"上传失败!");
}
}
}
我们做一个练习来加深对并发登陆的了解,现在有以下需求:客户端录入用户名,服务端对这个用户名进行校验,如果该用户名存在,在服务端显示“xxx,已经登陆”,在客户端则显示:“xxx,欢迎光临”。如果该用户名不存在,在服务端显示“xxx,尝试登陆”,在客户端显示“xxx,请注册”。客户端只能登陆三次,超过三次就挂掉。
/*
* 并发登陆系统:客户端
* 1.建立Socket
* 2.获取键盘录入
* 3.获取输出流
* 4.把录入数据写入输出流
* 5.获取输入流,判断服务端反馈信息,如果登陆上则关闭Socket
* 如果没有,继续获取录入并发送个服务端,三次为限。
* */
class Client {
public static void main(String[] args)throws Exception {
//建立Socket
Socket s = new Socket("127.0.0.1",8888);
//获取键盘录入
BufferedReader brkey = new BufferedReader(
new InputStreamReader(System.in));
//获取输出流
PrintWriter pwout = new PrintWriter(s.getOutputStream(),true);
//获取输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s.getInputStream()));
//限制次数
for(int i = 0;i<3;i++)
{
//获取键盘录入,如果录入为null,程序跳出
String line = brkey.readLine();
if(line ==null)
break;
//将读取到的数据写入输出流
pwout.println(line);
//获取服务端返回的数据
String lines = brin.readLine();
System.out.println("server:"+lines);
//如果已经登陆
if(lines.contains("欢迎"))
break;
}
s.close();
brkey.close();
}
}
/*
* 并发登陆系统:服务端,
*在本地建立一个文本文件来存储用户名, 服务端要通过查询文件来判断用户名是否存在
* 1.建立服务端,绑定端口号
* 2.获取客户端对象,开启一条线程
* 3.在线程类中封装以下内容
* 1>获取到客户端
* 2>获取输入流,读取客户端数据
* 3>获取输出流,发送数据
* 4>获取本地文件
* 5>查询本地文件判断用户名是否存在
* 如果存在则反馈成功信息给客户端,跳出循环,结束客户端
* 如果不存在则反馈失败信息给客户端,再继续读取客户端信息,三次为限
* 6>关闭相应资源。
* */
class Server {
public static void main(String[] args)throws Exception {
//建立服务端
ServerSocket ss = new ServerSocket(8888);
//获取服务端对象,不断开启线程
while(true)
{
Socket s = ss.accept();
new Thread(new PicThread(s)).start();
}
}
}
//线程类
class PicThread implements Runnable {
private Socket s;
PicThread(Socket s)
{
this.s = s;
}
public void run() {
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"...run time");
try {
//获取客户端输入流
BufferedReader brin = new BufferedReader(
new InputStreamReader(s.getInputStream()));
//定义获取本地用户户名信息的读取流
BufferedReader brcom =null;
//获取输出流
PrintWriter pwout = new PrintWriter(s.getOutputStream(),true);
//定义读取次数
for(int i=0;i<3;i++)
{
//每次客户端登陆的时候都需要去遍历本地信息
brcom = new BufferedReader(new FileReader("e://用户名.txt"));
//读取客户端的数据
String line = null;
line = brin.readLine();
//如果客户端发送了一个null过来,说明客户端结束了,这时不用读取,直接跳出
if(line==null)
break;
String name = null;
boolean flag = false;
//读取本地文件,如果存在,标记置为真,跳出循环
while((name = brcom.readLine())!=null)
{
if(name.equals(line))
{
flag = true;
break;
}
}
if(flag)
{
System.out.println(line+",已登陆成功");
pwout.println(line+",欢迎光临");
break;
}else
{
System.out.println(line+",尝试登陆");
pwout.println(line+",请注册");
}
}
//三次之后还没登陆上,关闭资源
s.close();
brcom.close();
} catch (Exception e) {
throw new RuntimeException(ip+"登陆错误");
}
}
}
(4)自定义浏览器访问TomCat服务器
TomCat服务器是纯java语言编写的一个服务器,他里面封装了ServerSocket,他的默认端口号是8080,当我们开启TomCat服务器,打开电脑上的任意浏览器,输入http://127.0.0.1:8080就能在浏览器中访问TomCat服务器。
虽然浏览器不是我们用java语言写的,但是他遵循一定的规则,比如IE浏览器访问服务器时,他会发给服务器一个消息头,这个消息头中包含着访问的格式,可接收的文件类型、语言等等信息。
我们通过自定义一个浏览器去访问TomCat服务器,当启动TomCat服务器后,再运行我们自定义的客户端就可以访问TomCat服务器了,这个示例可以把服务器反馈的信息存入到本地磁盘中。public class MyIE {
public static void main(String[] args) {
try {
//建立浏览器客户端,访问路径就写了TomCat服务器的路径
Socket s = new Socket("127.0.0.1",8080);
//获取输入流
PrintWriter pwout = new PrintWriter(s.getOutputStream(),true);
//写入消息头
pwout.println("GET / HTTP/1.1");//路径
pwout.println("Accept: */*");//文件格式
pwout.println("Accept-Language: zh-cn,is-IS;q=0.5");//语言
pwout.println("Host: 127.0.0.1:8080");//主机和端口号
pwout.println("Connection: closed");//结束标记
pwout.println();//注意要写空行和请求内容分开
//获取服务器发回的内容
InputStream is= s.getInputStream();
//把服务器返回的数据写入到本地文件中
PrintStream ps = new PrintStream(new FileOutputStream("e://tomcat.html"));
byte[] buf = new byte[1024];
int len = 0;
while((len = is.read(buf))!=-1)
{
ps.write(buf, 0, len);
}
//关闭资源
s.close();
ps.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三. URL
1. URL简介
URL是统一资源定位符的英文缩写,就是我们在浏览器上方常看到的一大串的路径地址,java把这个路径描述成一个对象,就是URL。
2. URL常用方法
1> getDefaultPort()
返回int型,获取与此 URL 关联协议的默认端口号。
2> getFile()
返回字符串,获取此 URL 的文件名。
3>getHost()
返回字符串,获取此 URL 的主机名(如果适用)。
4>getPath()
返回字符串, 获取此 URL 的路径部分。
5>getPort()
返回int型,获取此 URL 的端口号,当没有指定端口号时,此方法返回-1
6>getProtocol()
返回字符串,获取此 URL 的协议名称。
7>getQuery()
获取此 URL 的查询部分,一般在地址栏中我们会看到出来ip地址名和文件路径之外,还有一些信息以键值对形式存在,跟前面的内容以“?”连接。
演示:
class URLDemo1
{
public static void main(String[] args)throws MalformedURLException
{
//通过传入完整名称建立一个URL对象
URL url = new URL("http://127.0.0.1/myweb/demo.html?name=xiaohong&age=22");
System.out.println("获取协议:"+url.getProtocol());//http
System.out.println("获取主机名:"+url.getHost());//127.0.0.1
System.out.println("获取端口:"+url.getPort());//-1
System.out.println("获取与此 URL 关联协议的默认端口号:"+url.getDefaultPort());//80
System.out.println("获取此 URL 的路径部分:"+url.getPath());///myweb/demo.html
System.out.println("获取此 URL 文件:"+url.getFile());///myweb/demo.html?name=xiaohong&age=22
System.out.println("获取此 URL 的查询部分:"+url.getQuery());//name=xiaohong&age=22
}
}
3.URLConnection对象
URL中有一个方法openConnection,可以返回一个URLConnection对象,调用此方法可以连接URL所引用的远程对象,也就是每次调用这个方法都会进行连接,而且URLConnection中封装了Socket,他也可以获取到输入流和输出流。
这个类可以替代了Socket,Socket是在传输层,而此类已经上升到应用层,应用层是最终呈现在用户面前的,这一层的数据已经全部都拆封完毕,当我们做自定义浏览器访问Tomcat服务器示例的时候,使用的是Socket,在TomCat服务器返回的数据是传输层面上的,并不像我们用浏览器访问TomCat时所看到界面一样,这是因为Socket返回的数据还带有未拆分的响应头,而使用URLConnection访问服务器返回的数据就是全部拆分完后的数据,我们通过示例来看一下:
class URLDemo2 {
public static void main(String[] args)throws Exception {
//新建一个URL对象,指向黑马官网
URL u2 = new URL("http://www.itheima.com");
//调用openConnection方法连接url所指向的目标
URLConnection uc = u2.openConnection();
System.out.println(uc);
//获取输入流
InputStream is = uc.getInputStream();
//把服务端的数据写入到本地磁盘中
PrintStream ps = new PrintStream(new FileOutputStream("e://heima.html"));
byte[] buf = new byte[1024];
int len = 0;
while((len = is.read(buf))!=-1)
{
ps.write(buf,0,len);
}
//关
ps.close();
is.close();
/*我们打开e盘下的heima.html文件,和黑马的官网一样,
这就是利用URLConnetion获取服务端信息的特点。*/
}
}
四. 网络编程中的小知识
1.Socket的空参构造方法
在创建Socket对象的时候,我们一般都会指定他要连接的ip地址和端口号,但是Socket类中有一个空参的构造方法,如果我们创建了一个空参的Socket对象,可以通过connect(SocketAddress endpoint)方法来连接服务器。
SocketAddress是一个抽象类,他与InetAddress的区别在于它的出现是更多的是用于为Socket服务的,它的子类InetSocketAddress实现了IP套接字地址(IP地址+端口号),当Socket对象调用connect方法时就可以连接到指定的服务器了。
2.ServerSocket(int port,int backlog)
我们在创建服务端时,除了指定一个端口外,还可以限定客户端连接的数量,这个数量就是通过指定backlog的值来限定的。
3. 域名解析(DNS)
当我们在登陆百度服务器时,都是输入www.baidu.com就可以转到百度的页面,虽然我不清楚百度服务器的ip地址,但是我知道他的主机名就可以登陆服务器,这就是域名解析,其实浏览器还是访问的ip地址,但是他通过域名解析获取到ip地址,我们只需记得域名就可以了,而不用记住繁琐的ip地址。
在我们的电脑上,有一个专门用来存放ip地址和域名的映射关系的文件,就是c:\windows\system32\drivers\ext\host,这个文件中记录了一些基本的映射关系,比如我们本地回环地址127.0.0.1对应着localhost,我们在访问域名的时候先找这个文件中有没有对应的ip地址,有则直接返回给浏览器即可,如果没有就到DNS服务器中寻找对应的ip,然后交给浏览器访问该ip地址。
我们以访问百度为例说明:
(1)用户输入www.baidu.com
(2)去本地host文件中寻找是否有对应ip,有,直接返回,没有去DNS服务器列表中寻找。
(3) 找到ip后返回给浏览器
(4)浏览器获取到ip地址后通过ip地址访问指定服务端。
本地host文件是浏览器在访问DNS服务器之前去访问的,所以我们可以host文件有一些小作用。
(1)可以把经常访问的网址写入到host文件中
(2)把网址屏蔽掉,把网址对应本地ip:127.0.0.1