Java网络编程
温馨提示1:
(1)网络编程这一块会涉及到很多不同异常,得抛。
(2)涉及到iO,还是按照iO的思想来解决问题。
(3)接收端和发送端或者是服务端和客户端程序开启之后,两者的程序可以看 成是在相互切换着执行。因为程序中有阻塞式方法。
温馨提示2:
网络编程一般都是和io一起用,那么io有好多个对象。这里简单的记忆一下:
字符流:
(1)不加缓冲技术。读:read(byte[] buf),写:write(byte[] buf,0,len)
(2)加缓冲技术。读:readLine(),写:write(String str)
字节流:
(1)不加缓冲技术。读:read(byte[] buf),写:write(byte[] buf,0,len)
(2)字节流一般都不使用缓冲技术。
1、概述
之前我们将C盘下的内容拷到D盘下,都是单机版的,都在一台机器上。现在我们想将机器A的C盘内容拷到D盘下,就是网络版的,不在一台机器上。这就涉及到java中的网络编程,用java语言实现网络通讯。
一个很简单的例子:
张三在电脑上登录QQ与李四进行通讯。那么,张三用qq发的消息,李四也必须用qq接收。
因此,网络通讯三要素:
(1)IP地址。通过IP地址,我们才能找到对方。
IP地址分为4类,A、B、C、D类。每一个类IP地址有不同的地址段。
本地回环地址:172、0、0、1。测网卡时可以:ping 172.0.0.1.
保留地址:不用于公网,用于局域网中。而且如果两个局域网中都用
192、168.0.0。并不影响,不冲突。随着计算机越来越多,IP地址匮乏,
因此有IPv4向IPv6的过渡,但这个时期相当漫长,所以就有了子网划分。
(2)端口。数据要发送对方指定的应用程序上,为了标识这些应用程序,要给这些网络应用程序用数字进行标识。
端口号的取值范围:0~65535.一般0~1024被系统程序所保留,当然也可用,
只要保证不冲突。web服务器程序的端口号默认80,Tomcat服务器程序的端
口默认8080。
(3)通信规则。这个通讯规则称之为协议,双方装的协议必须一致,才能相互通信。国际组织定义了通用协议TCP/IP。其他的通信规则有http、udp等。
2、网络模型
(1)网络模型有OSI模型和TCP/IP模型
我们所处的层是应用层。
而网络编程所处的层是网络层和传输层。
(2)网络通讯的过程
发送方:
应用层-表示层-会话层-传输层-网络层-数据链路层-物理层:
数据每经过一层,该层根据本层的特点,给数据加一个特有标识,称之为
数据的打包。
接收方:
物理层-数据链路层-网络层-传输层-会话层-表示层-应用层:
数据每经过一层,该层根据本层的特点,给数据减一个特有标识,称之为数
数据的拆包。
3、IP地址
由于IP地址是点分十进制的形式,因此不好记忆,所以每一个IP地址都有一个对应的域名。如:http://www.baidu.com。
http:是一种协议。
www:万维网。
baidu:主机的名字。在万维网中注册。
com:该组织的属性,如商业性。
下面进入代码:
现在我们开始网络编程的学习,所用到的类都在java.net包中。
|--InetAddress
|--Inet4Adress
|--Inet6Adress
InetAddress类中没有构造函数,有静态方法和非静态方法,因此是单例设计模式。
(1)static InetAddress getLocalHost();返回本地主机。抛UnKnownException。
如:
import java.net.*;
class NetDemo
{
public static void main(String[] args)throws Exception
{
InetAddress i=InetAddress.getLocalHost();
System.out.println(i.toString());
}
}
运行结果:
(2)String getHostAddress():返回IP地址字符串。
(3)String getHostName():返回此IP地址的主机名。
现在,我们想分别获取,则:
import java.net.*;
class NetDemo
{
public static void main(String[] args)throws Exception
{
InetAddress i=InetAddress.getLocalHost();
System.out.println(i.getHostAddress());
System.out.println(i.getHostName());
}
}
运行结果:
以上(1)、(2)、(3)获取到的内容是从本地解析而来的。本地有这个IP地址和于明光的映射关系。现在我们想获取任意一台主机的相关内容。
(4)static InetAddress getByName(String host):
在给定主机名或者IP地址的情况下确定主机的 IP 地址或者主机名。
如:
import java.net.*;
class NetDemo
{
public static void main(String[] args)throws Exception
{
InetAddress i=InetAddress.getByName("192.168.0.1");
System.out.println(i.toString());
System.out.println(i.getHostAddress());
System.out.println(i.getHostName());
}
}
运行结果:
1)若没有连网,本地没有这样的映射关系的话,最后一项结果出来的会很慢,且i.getHostName()返回的仍然是host。
2)若连网了,网络上也仍然没有这样的映射关系,最后一项结果出来的一样会很慢,且i.getHostName()返回的仍然是host。
拓展:
static InetAddress[] getAllByName(String host):
在给定主机名的情况下,根据系统上配置的名称服务返回其 IP 地址所组成的数组。这种情况可以以百度举例,百度的服务器肯定不是一台。
4、TCP和UDP
(1)UDP
通信双方不需要确定对方是否存在以建立通信链接,发送方只管发,数据 能不能被收到无所谓。
因此UDP传输的特点:
1)将数据及源和目的封装成数据包,不需要建立链接。
2)每个数据包的大小限制在64k内。
3)因无链接所以是不可靠协议,容易丢包(通常指有效数据)。
4)因无链接所以速度快。
生活中的例子:快递、聊天
(2)TCP
通信双方必须确定对方是否存在以建立链接形成传输通道,这样才可以传送
数据。如果单方向断了,通道没有了,就不再传输数据了,因此就不会丢包。
(通常指有效数据)
生活中的例子:打电话、下载
5、Socket
Socket,中文意思:插座,插槽。在网络通信中又称之为“套接字”。之前我们说过网络通信其中的一个要素就是端口,而端口是数字,它是逻辑上的概念,其实它还需要被Socket绑定,Socket是物理上的概念。在Internet上的主机一般会运行多个服务软件,提供多个服务。每个服务对应一个网络应用程序,每个应用程序对应一个端口,该端口绑定了Socket。这样通信双方对应的Socket可以连接起来。(这种连接接不管是面向无连接的UDP传输还是面向连接接的TCP传输都是必须的。)
总结:
1)Socket就是为网络服务提供的一种机制。
2)通信的两端都有Socket。
3)网络通信其实就是Socket间的通信。
4)数据在两个Socket间通过IO传输。
6、UDP-发送和接收
网络通信按照区域划分可以分为两种:
|--本地通信。在本地的两个应用程序。
|--异地通信。
|--需要连网。不在同一个局域网中的两台计算机上的两个应用程序。
|--不需要连网。在同一个局域网中的两台计算机上的两个应用程序。
之前说过网络通信有三个要素:IP地址、端口、协议。IP地址我们已经说过它被封装成的类InetAddress,而协议这类事物很简单,不需要封装成类来创建对象。因此我们现在说一下端口。
我们知道端口,它和Socket绑定在一起,只有两端的Socket有了才能建立连接,传送数据。因此传送和接收都需要Socket,这个Socket负责接收和发送,还负责端口等。Socket很复杂,因此必须封装成对象,再抽取出一个类,叫做DatagramSocket。
还有之前讲过的网络模型,数据在传输的过程中,每经过一层,都会根据每一层的特点,给数据加上特有的标识,一层一层的封装。其中有IP地址,有端口号等一些内容,因此数据报包也是一类非常负责的事物,也封装成了对象,抽取出一个类叫做:DatagramPacket。
需要掌握的方法:
(1)DatagramSoket
1)DatagramSocket():构造数据报套接字并将其绑定到本地主机上任何 可用的端口。系统随机分配给该应用程序一端口。
重复发送或者接收,端口会从系统指定的这个端 口往下顺延使用。避免端口还未被释放而无法使 用。
2)DatagramSocket(int port):创建数据报套接字并将其绑定到本地主 机上的指定端口。(常用)
3)void close():关闭此数据报套接字。
4)void receive(DatagramPacket p):从此套接字接收数据包。阻塞式 方法,没有数据来就一直等。直 到有数据了,才醒过来。
5)void send(DatagramPacket p):从此套接字发送数据包。
(2)DatagramPacket
1)DatagramPacket(byte[] buf,int length):构造 DatagramPacket,用 来接收长度为 length 的数据包。
2)DatagramPacket(byte[] buf,int length,InerAddress address,int port):构造数据报包,用来将长度为 length 的包发送到指定主机上的指定 端口号。
3)InetAddress getAddress():返回某台机器的IP地址,此数据将要发往
机器或者是从该机器接收到的。
4)byte[] getData():返回数据缓冲区。
5)int getLength():返回将要发送或接收到的数据的长度。
6)int getPort():返回某台远程主机的端口号,此数据报将要发往该主机
或者是从该主机接收到的。
下面我们在本地模拟一下网络通信的过程。需要注意的是,我们的开两个dos控制台,不仅是因为接收端和发送端都是独立的应用程序,更是因为我们需要开启接收端和发送端来观察结果。两个dos控制台任意一个作为发送端的设备。
(1)UDP-发送端
新建一个文件,命名为UDPSend.java。
/*
定义一个应用程序,用于发送UDP协议传输的数据。
*/
import java.net.*;
class UDPSend
{
public static void main(String[] args)throws Exception
{
//创建端点。
DatagramSocket ds=new DatagramSocket();
//封装数据。
byte[] buf="udp ge men lai le".getBytes();
DatagramPacket dp=new DatagramPacket(buf,buf.length,InetAddress.getLocalHost(),10000);
//发送数据。
ds.send(dp);
//关闭端点。
ds.close();
}
}
(2)UDP-接收端
新建一个文件,命名为UDPReceive.java。
/*
定义一个应用程序,用于发送UDP协议传输的数据。
*/
import java.net.*;
class UDPReceive
{
public static void main(String[] args)throws Exception
{
//创建端点。
DatagramSocket ds=new DatagramSocket(10000);
//创建接收数据的数据包
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
//接收数据。
ds.receive(dp);
//对接收到数据进行处理。
String i=dp.getAddress().getHostAddress();
String data=new String(dp.getData(),0,dp.getLength());
int port=dp.getPort();
System.out.println(i+"::"+data+"::"+port);
//关闭端点。
ds.close();
}
}
ps:加粗部分的代码通常是合作的关系。两步缺一不可。
演示的时候,先将接收端开启(编译运行),此时没有数据发来,因此处于等待状态。再开启(编译运行)发送端,接收端的dos控制台会显示结果。
7、UDP-键盘录入
(1)在上一小节,我们知道了接收端和发送端数据传输的过程。但是,我们发现,发送端发送一次,接收端收到数据后就关闭了端点。发送端再次发送数据,接收端也无法接收到了。因此我们想,可以将接收端一直保持着开启的状态。接收端代码变化如下:
import java.net.*;
class UDPReceive
{
public static void main(String[] args)throws Exception
{
//创建端点。
DatagramSocket ds=new DatagramSocket(10001);
/*
创建端点不能放在whlie循环中,由于端口使用完不能立刻释放,会导致端口占用,发生BindException。
*/
while (true)
{
//封装数据。
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
//接收数据。
ds.receive(dp);
//对接收到数据进行处理。
String i=dp.getAddress().getHostAddress();
String data=new String(dp.getData(),0,dp.getLength());
int port=dp.getPort();
System.out.println(i+"::"+data+"::"+port);
}
//ds.close();
}
}
(2)以上我们实现了,发送端可以无限制的发送数据的功能。但是我们从结果来看,每次发送的内容都是一样的,我们现在想每次发送的内容不一样。
思考:
1)将内容改一下。不可取,将发送的内容改一下,那么还得重新编译,很麻烦。
2)用键盘录入形式录入不同的数据。因此用到iO流的技术。
代码实现过程:
a、发送端
import java.net.*;
import java.io.*;
class UDPSend
{
public static void main(String[] args)throws Exception
{
DatagramSocket ds=new DatagramSocket();
BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in));
String line=null;
while((line=bufr.readLine())!=null)//阻塞式方法
{
if("886".equals(line))
break;
byte[] buf=line.getBytes();
DatagramPacket dp=new DatagramPacket(buf,buf.length,InetAddress.getLocalHost(),10002);
ds.send(dp);
}
ds.close();
}
}
b、接收端
import java.net.*;
class UDPReceive
{
public static void main(String[] args)throws Exception
{
//创建端点。
DatagramSocket ds=new DatagramSocket(10002);
while (true)
{
//封装数据。
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
//接收数据。
ds.receive(dp);//阻塞式方法
//对接收到数据进行处理。
String i=dp.getAddress().getHostAddress();
String data=new String(dp.getData(),0,dp.getLength());
int port=dp.getPort();
System.out.println(i+"::"+data+"::"+port);
}
}
}
8、UDP-聊天
在上一小节的学习中,我们可以发现,接收端和发送端是两独立的应用程序,是两个进程,必须分别在两个dos控制台上开启。在接收端的控制台上只能看到接收到的内容;在发送端的控制台上只能看到发送端的内容。我们再想想qq的功能,当我们与一个聊天,在消息界面中,我们既能看到我们发送的消息,也能看到我们接收到的消息。而qq是一个进程,它包含了两个线程,一个接收,一个发送。
那么,现在我们可以简单的模拟一下:
import java.net.*;
import java.io.*;
class UDPSend implements Runnable
{
private DatagramSocket ds;
UDPSend(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("886".equals(line))
break;
byte[] buf=line.getBytes();
DatagramPacket dp=new DatagramPacket(buf,buf.length,InetAddress.getLocalHost(),10003);
ds.send(dp);
}
ds.close();
}
catch (Exception e)
{
throw new RuntimeException("发送失败!");
}
}
}
class UDPReceive implements Runnable
{
private DatagramSocket ds;
UDPReceive(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);
String ip=dp.getAddress().getHostAddress();
String data=new String(dp.getData(),0,dp.getLength());
int port=dp.getPort();
System.out.println(ip+"::"+data+"::"+port);
}
}
catch (Exception e)
{
throw new RuntimeException("接收失败!");
}
}
}
class ChatDemo
{
public static void main(String[] args)throws Exception
{
DatagramSocket sendDs=new DatagramSocket();
DatagramSocket receiveDs=new DatagramSocket(10003);
new Thread(new UDPSend(sendDs)).start();
new Thread(new UDPReceive(receiveDs)).start();
}
}
9、TCP传输
在UDP传输的学习中,我们知道了,网络通信有发送端和接收端(都是DatagramSocket)。那么在TCP传输中对应的有客户端和服务器端(Socket和ServerSocket)。
我们知道TCP是面向连接的传输方式。因此在Socket对象在建立时,就可以去连接指定主机。在建立Socket服务时,就要有服务端的存在,并连接成功,形成通路后,在该道进行数据的传输。具体来说,就是通路一建立,就有一个叫做Socket流,分为输入流和输出流,数据以流为载体,并随着网络发送端到指定的主机上。
我们知道,服务器不只连接一台客户端。当服务器同时连接好几台客户端的时候,如何保持数据的有效回馈呢?
服务器和客户端进行网络通讯,用的是客户端对象(Socket流)。并获取到该对象的输入或输出流对数据进行处理。这样就可以清楚服务器在和谁进行通讯。
在客户端中需要掌握的方法有:
1)Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
2)Socket()和void connect(SocketAddress andpoint):SocketAddress
的子类InetSocketAddress,
InetSocketAddress(InetAddress addr, int port)
根据 IP 地址和端口号创建套接字地址。
3)Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号。
4)InputStream getInputStream():返回此套接字的输入流。
5)OutputStream getOutputStream():返回此套接字的输出流。
6)write(byte[] buf):将 b.length 个字节从指定的 byte 数组写入此输出流。
7)void close():关闭此套接字。
在服务端中需要掌握的方法有:
1)ServerSocket(int port):创建绑定到特定端口的服务器套接字。
2)ServerSocket(int port,int backlog):backlog:能连接到服务器同时在线的客户端数目。
3)Socket accept():侦听并接受到此套接字的连接。阻塞式方法。
4)void close():关闭此套接字。(该动作可选,因为服务器是提供服务的,不可能说提供一次服务就关闭了,不科学。)
5)new ServerSocket().accept().getInputStream():使用客户端对象来和客户端进行通信。(此时是获取客户端对象的读取流,目的是为了读取客户端发来的数据。)
6)new ServerSocket().accept().getInetAddress().getHostAddress():获取客户端地址,来知晓到底是哪个客户端发来的消息。UDP传输中,IP地址直接封装在数据包中了。
7)new ServerSocket().accept().close():关闭客户端,因为客户端在占用服务端的资源,只要是连接超过一定的时间,就断开。
(1)简单的一个功能:客户端向服务端发送一次数据,服务器端在控制台上打印出相关信息。
客户端应用程序:
1)建立Socket服务。
2)获取Socket输出流对象。调用write(byte[] buf)方法,向服务端发送数据。
3)关闭Socket服务。
服务端应用程序:
1)建立ServerSocket服务。
2)获取客户端对象,调用getInetAddress()方法来获取客户端的地址;拿到客户端对象的读入流,调用read(byte[] buf)方法,读取客户端发来的数据并做处理。
3)关闭客户端(,关闭服务端)。
以上客户端和服务端是两个独立的应用程序,因此需要开启两进程。打开两个dos控制台,一个给客户端使用,一个给服务端使用。TCP中必须先开启服务端。
它们的代码实现如下图所示:
客户端:
import java.net.*;
import java.io.*;
class TCPClient
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost(),10004);
//获取Socket输出流对象,向服务端发送数据。
OutputStream out=s.getOutputStream();
byte[] buf="tcp ge man lai le".getBytes();
out.write(buf);
//关闭Socket服务。
s.close();
}
}
服务端:
import java.net.*;
import java.io.*;
class TCPServer
{
public static void main(String[] args)throws Exception
{
//建立ServerSocket服务。
ServerSocket ss=new ServerSocket(10004);
//获取客户端对象。获取客户端输入流对象。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+".....conneted");
byte[] buf=new byte[1024];
InputStream in=s.getInputStream();
int len=in.read(buf);
String str=new String(buf,0,len);
System.out.println(str);
//关闭Socket、ServerSocket服务。
s.close();
ss.close();
}
}
(2)在(1)的基础上,客户端向服务端发送一次数据,服务器端在控制台上打印出相关信息。并想客户端反馈一个信息。
客户端
{
先发;
后收;
}
服务端
{
先收;
后发;
}
代码实现如下:
客户端:
import java.net.*;
import java.io.*;
class TCPClient2
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost(),10005);
//获取Socket输出流对象向服务端发送信息。
OutputStream out=s.getOutputStream();
out.write("你好".getBytes());
//获取Socket输入流对象读取服务端反馈的信息。
InputStream in=s.getInputStream();
byte[] buf=new byte[1024];
int len=in.read(buf);
String str=new String(buf,0,len);
System.out.println(str);
//关闭Socket服务。
s.close();
}
}
服务端:
import java.net.*;
import java.io.*;
class TCPServer2
{
public static void main(String[] args)throws Exception
{
//建立ServerSocket服务。
ServerSocket ss=new ServerSocket(10005);
//获取客户端对象,拿到客户端读取流对象,读取客户端发送的消息。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+"....connect");
InputStream in=s.getInputStream();
byte[] buf=new byte[1024];
int len=in.read(buf);
String str=new String(buf,0,len);
System.out.println(str);
//获取客户端写入流对象,反馈给客户端消息。
OutputStream out=s. getOutputStream();
out.write("你也好".getBytes());
//关闭Socket服务,ServerSocket服务。
s.close();
ss.close();
}
}
练习:
文本传输
(1)需求:建立一个文本转化器,客户端通过键盘录入的方式给服务端发送文本,服务端会将文本转化成大写返回给客户端。客户端可以不断的进行文本转换,当客户端输入over时,转化结束。
分析:
从大的方面来讲:
源:客户端/服务端
目:服务端/客户端
从小的方面来讲:
客户端:
源:键盘
目:网络输入流。
服务端:
源:网络输入流
目:网络输出流
ps:这里需要注意的问题:
(1)用到字符缓冲写入流时,必须要刷新。
(2)客户端程序中,客户端通过键盘录入的方式给服务端发送了一行数据时:首先我们敲击键盘录入了一些数据,然后按下回车(\r\n),while循环条件中的阻塞式方法readLine()读取到一行文本,并且结束了。但是bufout.write()写进入的是回车符之前的数据,不包括回车符。那么到了服务端之后,仍然有while循环,循环条件中也有一个阻塞式方法readLine(),它就没有回车符可读,因此一直等待,以为还有数据要读。程序因此不能执行下去。所以在客户端程序中,bufout.write()方法应该将回车符写进去,有两种方法:1)在bufout.write(line+"\r\n");2)用BufferedWriter类中的一个很牛的方法:void newLine()。同理,服务器给客户端反馈信息时亦是如此。这类问题称之为结束标记问题。
(3)当客户端那边,控制台录入的是over,那么客户端和服务端都结束。服务端没有做判断却结束的原因是:客户端循环一结束,程序会往下执行,会执行关服务的代码。s被关闭了。相当于往服务端发送了一个结束标记。所以服务端的while循环也结束了。程序继续往下执行结束。
代码实现如下:
客户端:
import java.net.*;
import java.io.*;
class TCPClient3
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost().getHostAddress(),10006);
//定义要发送给服务端的文本,并发送给服务端。
BufferedWriter bufout=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader bufr=
new BufferedReader(new InputStreamReader(System.in));
BufferedReader bufin=
new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
if("over".equals(line))
break;
bufout.write(line);
bufout.newLine();
bufout.flush();
String str=bufin.readLine();
System.out.println("server:"+str);
}
//关服务。
bufr.close();
s.close();
}
}
服务端:
import java.net.*;
import java.io.*;
class TCPServer3
{
public static void main(String[] args)throws Exception
{
//建立ServerSocket服务。
ServerSocket ss=new ServerSocket(10006);
//获取客户端对象,接收客户端发来的数据并进行处理。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+"....connect");
BufferedReader bufin=
new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufout=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bufin.readLine())!=null)
{
//验证:客户端确实发送数据给服务端,而且服务端也接收到了。
System.out.println(line);
bufout.write(line.toUpperCase());
bufout.newLine();
bufout.flush();
}
//关服务。
s.close();
ss.close();
}
}
优化:
可以将客户端中的:
BufferedWriter bufout=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
改成:
PrintWriter out=new PrintWriter(s.getOutputStream(),true);
bufout.write(line.toUpperCase());
bufout.newLine();
bufout.flush();
改成:
out.println(line.toUpperCase());
(2)需求:复制文件。客户端向服务端发送一个文件。
代码实现:
客户端:
import java.net.*;
import java.io.*;
class TCPClient4
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost(),10008);
//准备数据,向服务端发送。以及接收服务端的反馈信息。
BufferedReader bufr=new BufferedReader(new FileReader("c:\\1.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
bufOut.write(line);
bufOut.newLine();
bufOut.flush();
}
String str=bufIn.readLine();
System.out.println("server:"+str);
//关闭服务。
bufr.close();
s.close();
}
}
服务端:
import java.io.*;
import java.net.*;
class TCPServer4
{
public static void main(String[] args)throws Exception
{
//建立服务。
ServerSocket ss=new ServerSocket(10008);
//接收客户端发来的收据,并反馈给客户端信息。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostName();
System.out.println(ip+"....connect");
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufw=new BufferedWriter(new FileWriter("C:\\2.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bufIn.readLine())!=null)
{
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufOut.write("上传成功!");
bufOut.newLine();
bufOut.flush();
bufw.close();
s.close();
ss.close();
}
}
这里会遇到的问题就是:
当你规规矩矩的将客户端和服务端程序写完以后。我们分别开启客户端和服务端,虽然文件已经上传到服务端,但是程序并没有结束。
原因:
这里仍然是阻塞式方法存在而导致:
public String readLine()
throws IOException
读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
返回:
包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null
抛出:
IOException - 如果发生 I/O 错误
当客户端已经读完了,服务端并不知道,还在等。服务端不能往下执行,就不能反馈给客户端消息。客户端的阻塞式方法在等。最终达到相互等待的状态。
我们解决的方案有两种:
(1)自定义结束标记。
客户端:
import java.net.*;
import java.io.*;
class TCPClient4
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost(),10008);
//准备数据,向服务端发送。以及接收服务端的反馈信息。
BufferedReader bufr=new BufferedReader(new FileReader("c:\\1.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
bufOut.write(line);
bufOut.newLine();
bufOut.flush();
}
bufOut.write("over");
bufOut.newLine();
bufOut.flush();
String str=bufIn.readLine();
System.out.println("server:"+str);
//关闭服务。
bufr.close();
s.close();
}
}
服务端:
import java.io.*;
import java.net.*;
class TCPServer4
{
public static void main(String[] args)throws Exception
{
//建立服务。
ServerSocket ss=new ServerSocket(10008);
//接收客户端发来的收据,并反馈给客户端信息。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostName();
System.out.println(ip+"....connect");
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufw=new BufferedWriter(new FileWriter("C:\\2.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bufIn.readLine())!=null)
{
if("over".equals(line))
break;
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufOut.write("上传成功!");
bufOut.newLine();
bufOut.flush();
bufw.close();
s.close();
ss.close();
}
}
但是,这个自定义的结束标记,有可能和源中的文件内容相同了,那么会导致文件不能完全复制过去。而且,这个标记客户端和服务端都必须知晓,因此更规范的做法也是客户端首先将这个结束标记给发过去,服务端每次拿这个标记来判断。
(2)时间戳
它是不唯一的。我们可以用时间来作为结束标记。
具体做法:
客户端中,我们在可以在发送文本之前发给服务端一个时间,服务端中首先来接受这个时间,把它作为结束标记。然后客户端向服务端发送文本,服务端在接收的同时,用if语句进行判断,看读到的内容是否与结束标记相同。然后客户端发完文本之后,再向服务端发送这个时间,服务端继续接受,可以结束while循环。
这里可以用到DataStream流,来处理这个时间。代码实现起来很麻烦,此处不作过多阐述。
(3)使用Socket中的方法。
客户端必须给服务端一个结束标记。可以使用方法:
void shutdownOutput():禁用此套接字的输出流。
客户端:
import java.net.*;
import java.io.*;
class TCPClient4
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket(InetAddress.getLocalHost(),10008);
//准备数据,向服务端发送。以及接收服务端的反馈信息。
BufferedReader bufr=new BufferedReader(new FileReader("c:\\1.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
bufOut.write(line);
bufOut.newLine();
bufOut.flush();
}
s.shutdownOutput();
String str=bufIn.readLine();
System.out.println("server:"+str);
//关闭服务。
bufr.close();
s.close();
}
}
服务端:
import java.io.*;
import java.net.*;
class TCPServer4
{
public static void main(String[] args)throws Exception
{
//建立服务。
ServerSocket ss=new ServerSocket(10008);
//接收客户端发来的收据,并反馈给客户端信息。
Socket s=ss.accept();
String ip=s.getInetAddress().getHostName();
System.out.println(ip+"....connect");
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufw=new BufferedWriter(new FileWriter("C:\\2.txt"));
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bufIn.readLine())!=null)
{
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufOut.write("上传成功!");
bufOut.newLine();
bufOut.flush();
bufw.close();
s.close();
ss.close();
}
}
通过上述的两个练习,我们可以总结出TCP传输的两个问题:
(1)由于阻塞式方法:readLine()的使用,所以我们必须注意一行的终止标记。主要在练习(1)中体现。
(2)由于阻塞式方法:readLine()的使用,所以我们必须注意文本的结束标记。主要在练习(2)中体现。
图片传输
(1)一个客户端,一个服务端(服务端只提供一次服务就关闭)
1)TCP-上传图片
客户端程序:
import java.net.*;
import java.io.*;
class PicTCPClient
{
public static void main(String[] args)throws Exception
{
/*建立Socket服务。*/
Socket s=new Socket(InetAddress.getLocalHost(),10009);
/*向服务端发送数据,从服务端接收数据。*/
//从本地获取一个图片文件。
FileInputStream fis=new FileInputStream("C:\\1.jpg");
//获取Socket输出流。向服务端发送数据。
OutputStream out =s.getOutputStream();
//获取Socket输入流。读取服务端反馈的消息。
InputStream in=s.getInputStream();
byte[] buf=new byte[1024];
int len=0;
while((len=fis.read(buf))!=-1)
{
out.write(buf,0,len);
}
//告诉服务端,已全部发送。
s.shutdownOutput();
byte[] bufIn=new byte[1024];
int lenIn=in.read(bufIn);
String str=new String(bufIn,0,lenIn);
System.out.println("server:"+str);
/*关闭服务*/
s.close();
fis.close();
}
}
服务端程序:
import java.io.*;
import java.net.*;
class PicTCPServer
{
public static void main(String[] args)throws Exception
{
/*创建ServerSocket服务*/
ServerSocket ss=new ServerSocket(10009);
/*接收客户端发来的数据,向客户端反馈信息*/
//获取客户端和服务端的连接。
Socket s=ss.accept();
//获取客户端对象的读取流,读取客户端发来的数据。
InputStream in=s.getInputStream();
//建立文件字节流,写入数据。
FileOutputStream fos=new FileOutputStream("C:\\2.jpg");
//获取客户端对象的写入流,给客户端反馈信息。
OutputStream out=s.getOutputStream();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+"....connected");
byte[] buf=new byte[1024];
int len=0;
while((len=in.read(buf))!=-1)
{
fos.write(buf,0,len);
}
out.write("上传成功!".getBytes());
fos.close();
s.close();
ss.close();
}
}
(2)多个客户端,一个服务端(服务端提供无限此服务,一直处于开启状态)
1)TCP-客户端并发上传图片
客户端:
a、我们想让客户端程序具有足够的跨平台性,我们就不能将要要发送给服务端的图片文件写死,我们应该根据不同机器上所拥有的图片文件来选择一个进行传递。那么想到main函数中的字符串数组参数args,客户端程序可以在运行的时候,传一个参数(所要传给服务端的图片文件名)。然后必须对这个参数进行若干个判断,比如,是否只传了一个参数,因为一次只能传一个文件;文件是否是文件且存在;是否是jpg格式的文件;文件大小是否在指定范围内,防止将avi格式的文件改成jpg格式的文件来进行上传。
服务端:
a、既然是多个客服端想与服务端连接,那么首先想到的是,服务端应该使用while(true){除了建立Socket服务的操作},且不用再关端点。但是,这样还是有局限性的,我们思考,当客户端A连进来以后,第一步被服务端获取到(这一步做完,服务端才能给客户端提供服务),然后必须按照执行流程往下执行。因此B连接进来,只能等服务器处理完A的请求以后,再进行while()下一次循环,才能被服务器获取到,给B提供服务。这样,根本不能实现并发的功能,因此想到多线程。服务端必须给每一个客户端一个单独的线程是比较合适的。定义线程,将客户端要在服务端进行的操作都放在run方法中去执行。以后开发时,服务器都得使用多线程。
b、还有就是服务端在接收客户端发来的图片,到底存什么样的名子是比较合适的呢?我们可以从两个方面来考虑:第一、想要每个客户端发来的文件存的时候都不能被覆盖,我们就不能把保存的文件名写死,我们可以将每个客户端的ip地址来作为文件名保存,因为ip是唯一的。第二、如果一个客户端发来多个文件的时候,还是会存在覆盖的问题,这时可以用计数器的思想通过(1)、(2)等数字进行标记。
代码如下:
1)客户端:
import java.net.*;
import java.io.*;
class PicThreadClient
{
public static void main(String[] args)throws Exception
{
/*建立Socket服务。*/
Socket s=new Socket(InetAddress.getLocalHost(),11000);
/*向服务端发送文件,并接收服务端的反馈信息*/
//判断程序运行时传入的参数是否能作为图片文件名而传给给字符读取流。
if((args.length)!=1)
{
System.out.println("请选择一个jpg格式的图片文件!");
return;
}
File file=new File(args[0]);
if(!(file.exists()&&file.isFile()))
{
System.out.println("该文件有问题,要么不存在,要么不是文件!");
return;
}
if(!(file.getName().endsWith(".jpg")))
{
System.out.println("图片格式错误,请重新选择!");
return;
}
if(file.length()>1024*1024*5)
{
System.out.println("文件过大,没安好心");
return;
}
//定义字节读取流。读取图片文件。
FileInputStream fis=new FileInputStream(file);
//获取Socket写入流,向服务端发送数据。
OutputStream out=s.getOutputStream();
//获取Socket读入流,读取服务端反馈的信息。
InputStream in=s.getInputStream();
byte[] buf=new byte[1024];
int len=0;
while((len=fis.read(buf))!=-1)
{
out.write(buf,0,len);
}
//告诉服务端,文件已经全部上传。
s.shutdownOutput();
byte[] bufIn=new byte[1024];
int lenIn=in.read(bufIn);
String str=new String(bufIn,0,lenIn);
System.out.println("server:"+str);
/*关服务*/
fis.close();
s.close();
}
}
2)服务端
import java.io.*;
import java.net.*;
class PicThreadServer
{
public static void main(String[] args)throws Exception
{
/*建立ServerSocket服务*/
ServerSocket ss=new ServerSocket(11000);
/*接收多个客户端发来的数据,并反馈给多个客户端信息*/
while (true)
{
Socket s=ss.accept();
new Thread(new PicThread(s)).start();
}
}
}
3)
import java.io.*;
import java.net.*;
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+"....connected");
try
{
//获取客户端读取流对象,读取客户端发来的信息。
InputStream in=s.getInputStream();
//定义一个文件字节写入流,用来存储客户端发来的文件。
int count=1;
File file=new File("C:\\"+ip+"("+count+")"+".jpg");
while(file.exists())
{
file=new File("C:\\"+ip+"("+(count++)+")"+".jpg");
}
FileOutputStream fos=new FileOutputStream(file);
//获取客户端写入流对象,反馈给客户端信息。
OutputStream out=s.getOutputStream();
byte[] buf=new byte[1024];
int len=0;
while((len=in.read(buf))!=-1)
{
fos.write(buf,0,len);
}
out.write("上传成功!".getBytes());
/*关闭服务*/
fos.close();
s.close();
}
catch (Exception e)
{
throw new RuntimeException(ip+"上传失败!");
}
}
}
2)TCP-客户端并发登录
需求:
客户端通过键盘录入用户名,服务端对这个用户名进行校验。
如果该用户存在,在服务端显示XXX,已登录,在客户端显示XXX,欢迎光临;如果该用户不存在,在服务端显示XXX,尝试登录,在客户端显示XXX,该用户不存在。
最多就登录三次。
分析:
首先,该功能的实现前提必须得有个用户名列表,也就是所谓的数据库。我们这里没有数据库,就简单做一个数据存储的地方,将所有的用户名都放在一个文本文件中。
客户端:
需求中要求最多只能登录三次,因此我们可以用for循环进行次数的限定。在循环中,我们先向服务端发送数据,再读取服务端反馈的信息。我们发送的数据是通过键盘录入的方式得到的,必须考虑到,如果我们按ctrl+c,那么这个数据就是null,那我们就没有必要向服务端再发送了,直接break;还有就是,如果反馈回来的信息中包含“欢迎”二字,那么我们就没有必要在登录了,因为已经登进入了,我们也要break。
服务端:
由于是多个客户端并发登录,因此也必须用到多线程技术,服务端必须为每一个客户端开启一个独立的线程。将原先服务端程序中的一些关键性代码放在run方法中。
run():
我们也可以用一个for循环来进行判断,来限定校验的次数(其实它主要的目的还是为了必要的时候在在其中写一个break结束掉对客户端的服务)。我们首先必须获取客户端读取流对象,来读取客户端发来的数据,将这个数据与用户名文件中的内容进行一一比较,因此我们还得建立一个文件字符读取流,来读取文件,用while循环,将每次读到的信息与这个数据进行比较。我们必须考虑,如果不匹配,该怎么办,匹配,又该怎么办。如果比较中了,直接break结束while循环,那么我们将最终的比较结果用一个标记记录,最后对这个标记进行判断。是怎么样,不是又怎么样,这是比较合理的。而不是直接在while()循环中进行最终判断。
客户端如果发来的是null,那么表明客户端已经不想再登录了,所以此时服务端对该客户端的服务也就结束了,所以for循环也应该break结束掉。
代码如下所示:
1)客户端:
import java.io.*;
import java.net.*;
class LoginClient
{
public static void main(String[] args)throws Exception
{
/*建立Socket服务*/
Socket s=new Socket(InetAddress.getLocalHost().getHostAddress(),11001);
/*向服务端发送数据,并接收服务端的反馈信息*/
//准备要向服务端发送的数据。
BufferedReader bufr=
new BufferedReader(new InputStreamReader(System.in));
//获取Socket写入流对象,向服务端发送数据。
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
//获取Socket读入流对象,接收服务端的反馈信息。
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
for(int x=1;x<=3;x++)
{
String line=bufr.readLine();
if(line==null)
break;
bufOut.write(line);
bufOut.newLine();
bufOut.flush();
String str=bufIn.readLine();
System.out.println(str);
if(str.contains("欢迎"))
break;
}
/*关服务*/
bufr.close();
s.close();
}
}
2)服务端:
import java.io.*;
import java.net.*;
class LoginServer
{
public static void main(String[] args)throws Exception
{
/*建立ServerSocket服务*/
ServerSocket ss=new ServerSocket(11001);
while(true)
{
Socket s=ss.accept();
new Thread(new LoginThread(s)).start();
}
}
}
3)run():
import java.io.*;
import java.net.*;
class LoginThread implements Runnable
{
private Socket s;
LoginThread(Socket s)
{
this.s=s;
}
public void run()
{
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+"...connected");
try
{
for(int x=1;x<=3;x++)
{
//获取客户端的读取流对象,读取客户端发来的数据。
BufferedReader bufIn=
new BufferedReader(new InputStreamReader(s.getInputStream()));
//建立文件字符读取流,读取文件中的内容,与客户端发送的内容进行比较。
BufferedReader bufr=
new BufferedReader(new FileReader("C:\\1.txt"));
//获取客户端电脑的写入流对象,向客户端发送反馈信息。
BufferedWriter bufOut=
new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String name=bufIn.readLine();
if(name==null)
break;
boolean flag=false;
String line=null;
while((line=bufr.readLine())!=null)
{
if(line.equals(name))
{
flag=true;
break;
}
}
if(flag)
{
System.out.println(name+",已登录");
bufOut.write(name+",欢迎光临");
bufOut.newLine();
bufOut.flush();
break;
}
else
{
System.out.println(name+",尝试登录");
bufOut.write(name+",用户不存在");
bufOut.newLine();
bufOut.flush();
}
}
s.close();
}
catch (Exception e)
{
throw new RuntimeException(ip+"校验失败");
}
}
}
综上所述,客户端和服务端无非就是基于网络的应用程序而已。客户端和服务端我们都可以自己定义。浏览器就是一个标准的客户端。
10、客户端:浏览器
1)浏览器客户端-自定义服务端
下面,我们就自己定义一个简单的服务端,打开浏览器去访问这个服务端。
s1:
import java.io.*;
import java.net.*;
class ServerDemo
{
public static void main(String[] args)throws Exception
{
/*建立ServerSocket服务。*/
ServerSocket ss=new ServerSocket(11002);
/*接收客户端的请求,并反馈信息*/
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip);
OutputStream out=s.getOutputStream();
out.write("欢迎光临".getBytes());
/*关闭服务*/
s.close();
ss.close();
}
}
s2:在dos命令行中编译运行,处于等待状态。
s3::打开IE,在地址栏输入:http://172.117.53.217:11002,按回车,结果如下:
由于客户端是浏览器,它具有解析的功能。
因此我们可以给字体加一些效果:
s1:
import java.io.*;
import java.net.*;
class ServerDemo
{
public static void main(String[] args)throws Exception
{
/*建立ServerSocket服务。*/
ServerSocket ss=new ServerSocket(11002);
/*接收客户端的请求,并反馈信息*/
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip);
PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
pw.println("<font color='red' size='7' >欢迎光临</font>");
/*关闭服务*/
s.close();
ss.close();
}
}
s2:在dos命令行中编译运行,处于等待状态。
s3:打开IE,在地址栏输入:http://172.117.53.217:11002,按回车,结果如下:
拓展:还有一个标准的客户端就是telnet。它是windows提供的一个远程登录工具,可以在dos控制台中去连接网络中任意一台主机 。连接以后,可以对这台主机进行命令式的配置。比如,我们在玩网络的时候,网络中有很多交换机和路由器,那些设备都是可以配置的。我们可以通过telent命令远程登录过去,在本地进行路由器和交换机的配置。它不具有解析功能。
比如:
现在,我们可以再开启一个dos控制台,在控制台上输入命令:
telnet 172.117.53.217空格11002回车。
2)浏览器客户端-Tomcat服务端
Tomcat服务器是纯java编写的,它里边必然是封装了ServerSocket服务。
s1:打开Tomcat服务器。(已安装的软件)。出现一个dos控制台。可以看到它封装的端口是8080,。
s2:打开IE,在地址栏中输入地址:http://172.117.53.217:8080,回车。
s3:IE中出现了一个网页。(是别人做好的)
现在,我们想让打开的网页是我们自己做的。
浏览器和Tomcat还是得开着。我们在本地(假设是F:\tomcat\webapps)新建一个文件夹,叫作myweb;文件夹中新建一个html格式的文本文件。该文本文件中写的内容是:
<html>
<body>
<h1>这是我的主页</h1>
<font color=red size=7>欢迎光临</font>
<div>
窗前明月光</br>
疑是地上霜</br>
举头万明月</br>
低头思故乡</br>
</div>
</body>
</html>
然后我们在浏览器的地址栏中输入:http//172.117.53.217:8080/myweb/demo.html,回车之后,打开的就是我们自己定义的页面。
Tomcat服务器最大的特点就是:它能读取我们自定义的一些内容,并在客服端请求服务的时候,将这些自定义内容反馈给客户端。
3)自定义浏览器-Tomcat服务端
在“浏览器客户端-Tomcat服务端”小节中,我们应当思考,浏览器到底发了什么内容给Tomcat服务器,Tomcat服务器能够给它反馈所需要的信息呢?
我们先用自定义的服务端,和浏览器客户端来测试一下,浏览器给服务器发了什么内容。
s1:自定义服务端
import java.io.*;
import java.net.*;
class ServerDemo
{
public static void main(String[] args)throws Exception
{
//建立服务。
ServerSocket ss=new ServerSocket(11111);
//获取连接
Socket s=ss.accept();
//获取客户端的读取流对象。
InputStream in=s.getInputStream();
//获取客户端的写入流对象。
OutputStream out=s.getOutputStream();
//读取客户端发来的数据。
byte[] buf=new byte[1024];
int len=in.read(buf);
System.out.println(new String(buf,0,len));
//反馈给客户端信息。
out.write("客户端你好".getBytes());
//假设服务器只提供一次服务。服务完就关闭。
s.close();
ss.close();
}
}
s2:打开dos控制台,编译运行后,处于等待状态。
s3:打开IE浏览器,在地址栏输入:http://172.117.91.132:11111,回车后,显示结果如下图:
s4:查看dos控制台,显示结果如下:
解析:
那么,现在我们也搞一个客户端,向Tomcat发送这些内容。做一个山寨版的浏览器。
(1)用浏览器作为客户端,用Tomacat服务器作为服务端:
s1:进入:F:\tomcat\bin,双击Tomcat6.exe。
s2:打开IE浏览器,在地址栏输入:http://172.117.91.132:8080/myweb/demo.html,回车。
s3:客户端显示结果如下:
(2)自定义客户端,用Tomacat服务器作为服务端:
s1:自定义客户端:
import java.io.*;
import java.net.*;
class MyIE
{
public static void main(String[] args)throws Exception
{
//建立Socket服务。
Socket s=new Socket("172.117.91.132",8080);
//获取Socket写入流。
PrintWriter pw=new PrintWriter(s.getOutputStream());
//向服务端发送请求内容。
pw.println("GET/myweb/demo.html HTTP/1.1");
pw.println("Accept:*/*");
pw.println(""172.117.91.132":8080");
pw.println("Accept-Language:zh-cn");
pw.println(""Connection:Keep-Alive");
pw.println();
pw.println();
//获取Socket读入流。
BufferedReader bufr=
new BufferedReader(new InputStreamReader(s.getInputStream()));
//读取服务端反馈的信息。
String line=null;
while((line=bufr.readLine())!=null)
{
System.out.println(line);
}
//关闭服务。
s.close();
}
}
ps:这里需要注意的就是,客户端需要结束标记,因为这里用了readLine()阻塞式方法,所以一时半会儿停不下来,除非超时。我们有两种解决办法:
一、将Keep-Alive改成closed。
二、不用缓冲技术,不用readLine(),直接使用字节读取流,用read(byte[] buf)。
s2:进入:F:\tomcat\bin,双击Tomcat6.exe,打开Tomcat服务器。
s3:打开dos控制台,编译运行。结果如下:
ps:我们都知道,像一些浏览器、文本编辑器都支持utf-8编码,因此都能正确的显示出来。但是dos控制台,它是系统的,相当于系统中的一个软件,它不支持utf-8编码,因此它显示不出本来的内容,我们只要将本来的内容改成是GB2312中文编码,就可以了。
温馨提示:本机的ip地址可以通过在dos控制台中输入:ipconfig命令获取。
4)自定义图形界面浏览器-Tomcat服务端
接下来,我们把我们自定义的浏览器做成一个界面。毫无疑问就是GUI技术。
s1:自定义客户端。
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
class MyIEByGUI
{
private Frame f;
private TextField tf;
private Button but;
private TextArea ta;
MyIEByGUI()
{
init();
}
public void init()
{
f=new Frame("浏览器");
but=new Button("转到");
tf=new TextField(60);
ta=new TextArea(25,70);
f.setLayout(new FlowLayout());
f.setBounds(300,100,600,500);
f.add(tf);
f.add(but);
f.add(ta);
myEvent();
f.setVisible(true);
}
public void myEvent()
{
f.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
but.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
});
tf.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
try
{
if(e.getKeyCode()==KeyEvent.VK_ENTER)
showDir();
}
catch (Exception ex)
{
}
}
});
}
public void showDir()throws Exception
{
//每连接一次,就清空一次。
ta.setText("");
String url=tf.getText();//http://127.0.0.1:8080/myweb/demo.html
int index1=url.indexOf("//")+2;
int index2=url.indexOf("/",index1);
String str=url.substring(index1,index2);
String[] arr=str.split(":");
String host=arr[0];
int port=Integer.parseInt(arr[1]);
String path=url.substring(index2);
Socket s=new Socket(host,port);
PrintWriter out=new PrintWriter(s.getOutputStream(),true);
out.println("GET "+path+" HTTP/1.1");
out.println("Accept: */*");
out.println("Accept-Language:zh-cn");
out.println("Host: 192.168.1.254:11000");
out.println("Connection:Keep-Alive");
out.println();
out.println();
BufferedReader bufr=
new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
ta.append(line+"\r\n");
}
s.close();
}
public static void main(String[] args)
{
new MyIEByGUI();
}
}
s2:打开Tomcat服务器。
s3:打开dos控制台,编译运行,结果如下:
5)URL-URLConnection
在上一节中,我们实现了自定义的图形化界面浏览器访问Tomcat服务器,确实也请求到客户端所需要的东西了。但是,服务端反馈的信息中不仅仅是客户端所请求的内容,还包括应答消息头。
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"200-1461244373587"
Last-Modified: Thu, 21 Apr 2016 13:12:53 GMT
Content-Type: text/html
Content-Length: 200
Date: Fri, 22 Apr 2016 07:16:05 GMT
这是和浏览器访问所得到结果的不同之处。
原因就是:数据从发送端发出,是经过一层层封装的,每一层根据自己的协议,给数据加一些特有信息,再封装起来,传到下一层。而接收端接收的时候,是进过一层层拆封的,每一层根据自己的协议,把自己本层在数据中的特有信息给去掉。我们刚才自己定义的浏览器,所处的位置是传输层,而真正的浏览器客户端所在的位置是应用层。因此用真正浏览器客户端访问,数据传输得多传输一层,那么消息头就会被拆掉。简而言之:一个走的是应用层协议,一个走的是传输层的协议。
那么我们自己定义的浏览器客户端怎么去实现这一步呢?
在java.net包中提供了一个很牛的类:URL。(统一资源定位符),它还有一个相似的类:URI,它的范围比URL大,像一些条形码也包括在内。它走的是应用层。
URL
(1)URL的用处之一:获取一些信息。
URL(String space):根据 String 表示形式创建 URL 对象。
URL类中必须掌握的方法:
1)String getFile():获取此 URL 的文件名。
2)String getHost():获取此 URL 的主机名。
3)String getProtocol():获取此 URL 的协议名称。
4)int getQuery():获取此 URL 的查询部分。
5)String getPort():获取此 URL 的端口号。
6)String getPath():获取此 URL 的路径部分。
示例:
import java.net.*;
class URLDemo
{
public static void main(String[] args)throws Exception
{
URL url=new URL("http://127.0.0.1:8080/myweb/demo.html");
System.out.println("getProtocol:"+url.getProtocol());
System.out.println("getFile:"+url.getFile());
System.out.println("getHost:"+url.getHost());
System.out.println("getQuery:"+url.getQuery());
System.out.println("getPort:"+url.getPort());
System.out.println("getPath:"+url.getPath());
/*
int port = url.getPort();
if(port==-1)
port=80;
*/
}
}
运行结果:
getFile()和getPath()是有区别的:
如果URL封装的是:
"http://127.0.0.1:8080/myweb/demo.html?name=haha&age=30"
运行结果是:
由上可知,我们都是先获取url的一些信息,然后进行Socket封装的。
(2)替代Socket对象,获取到流对象。
7)URLConnection openConnnection():返回一个 URLConnection 对象,它表示到 URL 所引用的远程对象的连接。
URLConnection conn=url.openConnection();客户端和服务端连接。
InputStream in=conn.getInputStream();
示例:
import java.net.*;
import java.io.*;
class URLConnectionDemo
{
public static void main(String[] args)throws Exception
{
URL url=new URL("http://127.0.0.1:8080/myweb/demo.html");
URLConnection conn=url.openConnection();
InputStream in=conn.getInputStream();
byte[] buf=new byte[1024];
int len=in.read(buf);
System.out.println(new String(buf,0,len));
}
}
运行结果:
发现,响应头没有了。
现在,我们将之前的自定义图形化界面浏览器再优化一下:
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
class URLConnectionByGUI
{
private Frame f;
private Button but;
private TextField tf;
private TextArea ta;
URLConnectionByGUI()
{
init();
}
public void init()
{
f=new Frame("浏览器");
tf=new TextField(60);
but=new Button("转到");
ta=new TextArea(25,70);
f.setBounds(300,100,600,500);
f.setLayout(new FlowLayout());
f.add(tf);
f.add(but);
f.add(ta);
myEvent();
f.setVisible(true);
}
public void myEvent()
{
f.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
tf.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
try
{
if(e.getKeyCode()==KeyEvent.VK_ENTER)
showDir();
}
catch (Exception ex)
{
}
}
});
but.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
});
}
public void showDir()
{
try
{
ta.setText("");
String str=tf.getText();
URL url=new URL(str);
URLConnection conn=url.openConnection();
BufferedReader bufr=
new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null)
{
ta.append(line+"\r\n");
}
}
catch (Exception e)
{
}
}
public static void main(String[] args)
{
new URLConnectionByGUI();
}
}
运行结果:
小知识点:
8)InputStream openStream():打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。
它其实就是openConnection().getInputStream()的缩写。
6)域名解析
现在我们来理清一下:当我们在浏览器的地址栏中输入:
http://127.0.0.1:8080/myweb/demo.html,按下回车,浏览器它到底都做了那些事。
首先,浏览器解析完这句话以后,先看协议,之后去启动相对应的协议,并解析后面的主机和端口号,将它们封装成Socket对象,然后进行数据传输。
主机是ip地址形式出现的,就直接被解析。这个不做过多阐述。如果主机是主机名形式出现的,那么浏览器会先去本地C:\Windows\System32\drivers\etc目录下的hosts文件中找是否有与之对应的ip地址。有就直接获取到ip地址,没有浏览器就向与它所配置的dns服务器发出请求,请求解析地址。
拓展:
打开hosts文件:
截取部分内容:
这个文件主要有以下几个作用:
1)我们可以将我们经常要访问的服务器在这里边添加,这样下次我们访问这个服务器时,应答就会迅速一点。
2)我们可以用127.0.01这个ip地址来屏蔽我们不想自动访问的页面,如广告页面。将这些网址都和127.0.0.1映射。这样就连不出去。
2016-04-17至
2016-02-22著