1、网络模型:OSI参考模型和TCP/IP参考模型
图示:
一般来说开发处于传输层和网际层,应用层为:FTP和HTTP协议等,传输层为:UDP和TCP等,网际层为:IP。
通常用户操作的是应用层,而编程人员需要做的是传输层和网际层,用户在应用层操作的数据,经过逐层封包,最后到物理层发送到另一个模型中,再进行逐层解包,图示为:
2、网络通信三要素:IP地址,端口号,传输协议
A、IP地址
a、它是网络中的设备标识
b、不易记忆,可用主机名表示,两者存在映射关系
c、本机回环地址:127.0.0.1,主机名为:localhost。
IP地址:java中对应的是InetAddress类,存在于java.net包中。
InetAddress类:
(一)无构造函数,可通过getLocalHost()方法获取InetAddress对象,此方法是静态的,返回本类对象。
InetAddress i = InetAddress.getLocalHost();
(二)方法:
1)static InetAddress getByName(String host):获取指定主机的IP和主机名。(最好用ip地址去获取,主机名需要解析)
2)static InetAddress[] getAllByName(String host):在给定主机名的情况下,根据系统上配置的名称服务返回IP地址所组成的数组。返回对象不唯一时,用此方法。
3)String getHostAddress():返回IP地址字符串文本形式,以IP地址为主。
4)String getHostName():返回IP地址主机名。
(三)如何获取任意一台主机的IP地址对象:
1)功能:返回InetAddress对象
2)对于任意主机,需要指定传入主机名的参数
注意:如果IP地址和对应的主机名,这种映射关系没有在网络上,就不会解析成功,返回的还是指定的IP。
示例:
/*InetAddress练习*/ import java.net.*; class InetAddressDemo { public static void main(String[] args) { try { //练习一、获取本机信息 //通过getLocalHost("String");获取本地主机 InetAddress inet = InetAddress.getLocalHost(); //如果找不到 host 的任何 IP 地址。抛出UnknownHostException异常 //通过InetAddress对象获取本地主机IP sop(inet.getHostAddress()); //通过InetAddress对象获取本地主机名 sop(inet.getHostName()); //练习二、指定IP获取信息 /* static InetAddress getByName(String host) 在给定主机名的情况下确定主机的 IP 地址。 */ /* InetAddress inet1 = InetAddress.getByName("196.168.1.254"); sop("inet1="+inet1.getHostAddress()); sop("inet1="+inet1.getHostName()); */ InetAddress inet1 = InetAddress.getByName("www.baidu.com"); sop("inet1="+inet1.getHostAddress()); sop("inet1="+inet1.getHostName()); //练习三、指定IP获取信息 /* static InetAddress[] getAllByName(String host) 在给定主机名的情况下,根据系统上配置的名称服务返回其 IP 地址 所组成的数组。 */ InetAddress [] inet2 = InetAddress.getAllByName("www.baidu.com"); for(InetAddress in:inet2){ sop("inet2="+in.getHostAddress()); sop("inet2="+in.getHostName()); } } catch (UnknownHostException e) { } } public static void sop(Object obj){ System.out.println(obj); } }
B、端口号:
a、用于标识进程的逻辑地址,不用进程的标识。
b、有效端口:0 ~65535,系统使用或保留的端口是:0~ 1024。
C、传输协议:
即通信规则,包含TCP和UDP协议
UDP
是面向无连接,明确了对方的端口,无论在不在网上,只管传输,不在就会丢失数据。只求速度,应用于网络视频会议和聊天等应用程序中。
协议特点:
a、面向无连接,即将数据及源和目的封装成数据包中,不建立链接的发送
b、每个数据包的大小限制在64K之内
c、因无连接,是不可靠的协议
d、不建立连接,速度快。
TCP
是面向连接的,必须连接成功才能传输数据,应用于下载等程序上
协议特点:
a、面向连接,在建立连接后,形成传输数据的通道
b、在连接中进行大数据量的传输
c、通过三次握手完成连接,是可靠的协议
d、必须建立连接,效率稍慢
三次握手:第一次本方发送请求,第二次对方确认连接,第三次本方再次确认连接成功。
3、通信的步骤:
1)找到IP地址
2)数据要发送到对象指定应用程序,为标识这些应用程序,所以给这些网络应用程序都用数字标识,为方便称呼这个数字,叫做端口,即逻辑端口。
3)定义通信规则,称之为协议。国际组织定义了通用协议,即TCP/IP。
注意:必须要有数字标识才能将数据发送到应用程序上。
传输协议
一、Socket
1、它被称之为插座,相当于港口一样,是网络服务提供的一种机制。
2、通信两端都要有Socket,才能建立服务。
3、网络通信其实就是Socket间的通信,数据在两个Socket间通过IO传输。
二、UDP传输
1、通过类DatagramSocket,此类表示用发送和接收数据包的套接字,即Socket。
2、方法:
1)创建 UDPSocket发送服务对象:
DatagramSocket(),不指定端口。DatagramSocket(int port),指定端口。
2)发送:void send(DatagramPacket p)
3)接收:void receive(DatagramPacket p)
其中DatagramPacket:数据报包用来实现无连接包投递服务的,每条报文仅根据该包中包含的信息从一台机器路由到另一台机器中。凡是带地址(InetAddress)的都是用于发送包的。
3、步骤
1)发送数据:
a、建立UDPSocket服务,在此无需指定端口,也可以将端口加入。如果不指定的话,系统会随机分配一个端口,如第一次运行时端口为1093,那么第二次就会顺延为1094,再运行会一直顺延,因为之前的端口还没有得到释放,所以会顺延端口号值。
b、提供数据,并将数据封装到数据包中
c、通过socket服务的发送功能,将数据包发送出去
d、关闭资源
2)接收数据:
a、定义UDPSocket服务。通常会监听一个端口,其实就是给这个接收网路应用程序定义数字标识,方便于明确哪些数据过来该应用程序可以处理。
b、定义一个数据包,用来存储接收到的字节数据,因为数据包对象中有更多功能可以提取字节数据中的不同数据信息。
c、通过socket服务的receive方法接收到的数据存入已定义好的数据包中
d、通过数据包对象的特有功能,将这些不同的数据取出,打印在控制台上
e、关闭资源
在定义接收数据的方法中,仍会在DatagramSocket构造函数中传入DatagramPacket的参数,这是因为收到的数据太多,需要解析,通过将数据封装成对象,易于解析,所以需要传入参数。
注意:
1、发送端与接收端是两个独立的运行程序。
2、在发送端,要在数据包对象中明确目的地IP及端口。
3、在接收端,要指定监听的端口。
示例:练习一、接收端接收发送端键盘录入内容,并打印/*UDP传输 */ /*发送端 步骤:1、建立UDP的socket服务,也就是端点 2、将要提供的数据封装到数据包中 3、通过socket服务的发送功能将数据发出 4、关闭资源 */ import java.net.*; class DatagramSocketSend { public static void main(String[] args) throws Exception { //创建UDP的socket服务 DatagramSocket out = new DatagramSocket(); //要发送的数据 byte [] data = "UDP come to Be".getBytes(); //建立数据包,实现无线投递服务 DatagramPacket pack = new DatagramPacket(data,data.length,InetAddress.getByName("192.168.1.255"),10010); //通过socket服务的sent方法将数据发出 out.send(pack); //关闭资源 out.close(); } } /*接收端 步骤:1、创建UDP接收端服务,并指定端口号 2、定义一个数据包,用来存储接收到的字节数据, 因为数据包对象中有更多功能可以提取字节数据中的不同数据信息 3、通过Socket服务的receive方法将接收到的数据存入已定义好的数据包中 4、使用数据包的特有功能,对这些不同的数据取出,并打印到控制台 5、关闭资源 */ class DatagramSocketReceive { public static void main(String[] args) throws Exception { //创建socket服务,并指定端口号为10000,方便测试 DatagramSocket ds = new DatagramSocket(10010); while(true){ //定义一个数据包,用于存储接收到的数据 byte [] buff = new byte[1024]; DatagramPacket pack = new DatagramPacket(buff,buff.length); //通过socket服务的receive方法将接收到的数据存入数据包 ds.receive(pack); //该方法是阻塞状态 //获取信息 //获取发送端IP System.out.println(pack.getAddress().getHostAddress()); //获取发送端端口号 System.out.println(pack.getPort()); //获取发送端发送的数据的长度 System.out.println(pack.getLength()); //获取发送端发送的数据 System.out.println(new String(pack.getData(),0,pack.getLength())); } } }
练习二、/* 需求:把键盘输入的数据,在接收端显示 */ /* 发送端: 步骤: 1、创建UDP的socket服务 2、定义字符输出流,读取键盘输入的数据 3、定义数组保存键盘输入数据 4、使用socket服务的send方法将数据发出 5、关闭资源 */ import java.io.*; import java.net.*; class DatagramSystemSend { public static void main(String[] args) throws Exception { //1、 DatagramSocket out = new DatagramSocket(); //2、 BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //3、 String line = ""; while((line=bufr.readLine())!=null){ if("over".equals(line)){ line = "发送端发送完毕!!!"; //4 send(line,out); break; } else send(line,out); } //5 out.close(); bufr.close(); } public static void send(String line,DatagramSocket out)throws IOException{ byte [] data = line.getBytes(); DatagramPacket pack = new DatagramPacket(data,data.length,InetAddress.getByName("192.168.1.255"),10005); out.send(pack); } } /* 接收端: 步骤: 1、创建UDP的socket服务 2、定义数据包用于保存从发送端读取到的数据 3、使用socket的receive方法将读取到的数据保存到数据包 4、使用数据包的功能取出数据,并打印在控制台 5、关闭资源 */ class DatagramSystemReceive { public static void main(String[] args) throws Exception { //1、 DatagramSocket in = new DatagramSocket(10005); while(true){ //2、 byte [] data = new byte[1024]; DatagramPacket pack = new DatagramPacket(data,data.length); //3、 in.receive(pack); System.out.println(data.length); //4、 //获取发送端发送数据的长度 int length = pack.getLength(); //获取发送端IP String address = pack.getAddress().getHostAddress(); //获取发送端端口号 int port = pack.getPort(); //获取发送端数据 String str = new String(pack.getData(),0,length); System.out.println(address+"......."+port+"。。。。。"+length); System.out.println(str); if(str.equals("发送端发送完毕!!!")) break; } in.close(); } }
/* 编写一个聊天程序。 有收数据的部分,和发数据的部分。 这两部分需要同时执行。 那就需要用到多线程技术。 一个线程控制收,一个线程控制发。 因为收和发动作是不一致的,所以要定义两个run方法。 而且这个两个方法要封装到不同的类中。 */ //Udp发送线程 import java.net.*; import java.io.*; class UdpSend implements Runnable { //定义Socket服务引用 private DatagramSocket ds; UdpSend(DatagramSocket ds) { this.ds=ds; } //复写run方法 public void run() { try { //2、确定数据,从键盘录入,并把键盘录入的数据封装成数据包 DatagramPacket dp=null; BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); String line=null; while((line=br.readLine())!=null) { byte[] buf=line.getBytes(); dp=new DatagramPacket(buf,buf.length,InetAddress.getByName("192.168.1.255"),10000); //3、通过Socket服务,将已有的数据包发送出去 ds.send(dp); if ("886".equals(line)) { break; } } //4、关闭资源 ds.close(); } catch (Exception e) { throw new RuntimeException("发送数据失败"); } } } //Udp接收线程 class UdpReceive implements Runnable { //定义Socket服务引用 private DatagramSocket ds; UdpReceive(DatagramSocket ds) { this.ds=ds; } //复写run方法 public void run() { try { //一直处于接收状态 while (true) { //2、定义数据包,用于存储数据 byte[] buf=new byte[1024]; DatagramPacket dp=new DatagramPacket(buf,buf.length); //3、通过Socket服务,将数据接收并存储进数据包中 ds.receive(dp); //4、通过数据包的方法获取其中的数据 String ip=dp.getAddress().getHostAddress(); String data=new String(dp.getData(),0,dp.getLength()); int port=dp.getPort(); System.out.println("IP:"+ip+"=="+data); } //5、关闭资源 //ds.close(); } catch (Exception e) { throw new RuntimeException("接收端接收数据失败"); } } } class UdpChatDemo { public static void main(String[] args)throws Exception { new Thread(new UdpSend(new DatagramSocket())).start(); new Thread(new UdpReceive(new DatagramSocket(10000))).start(); } }
三、TCP传输
1、TCP分客户端和服务端。客户端对应的对象是Socket,服务端对应的对象是ServerSocket。
2、方法:
1)创建客户端对象:
Socket():创建空参数的客户端对象,一般用于服务端接收数据
Socket(String host,int port),指定要接收的IP地址和端口号
2)创建服务端对象:ServerSocket(int port):指定接收的客户端的端口
3)Socket accept():监听并接受到此套接字的连接
4)void shutdownInput():此套接字的输入流至于“流的末尾”
5)void shutdownOutput():禁用此套接字的输出流
6)InputStream getInputStream():返回此套接字的输入流,Socket对象调用
7)OutputStream getOutputStream():返回套接字的输出流,Socket对象调用
3、基本思路
客户端:
1)客户端需要明确服务器的ip地址以及端口,这样才可以去试着建立连接,如果连接失败,会出现异常。
2)连接成功,说明客户端与服务端建立了通道,那么通过IO流就可以进行数据的传输,而Socket对象已经提供了输入流和输出流对象,通过getInputStream(),getOutputStream()获取即可。
3)与服务端通讯结束后,关闭Socket。
服务端:
1)服务端需要明确它要处理的数据是从哪个端口进入的。
2)当有客户端访问时,要明确是哪个客户端,可通过accept()获取已连接的客户端对象,并通过该对象与客户端通过IO流进行数据传输。
3)当该客户端访问结束,关闭该客户端。
4、步骤
客户端:
通过查阅Socket对象的API文档,发现在该对象在建立时,就可去连接指定主机,因为TCP是面向连接的,所以在建立Socket服务时,就要有服务端存在,并连接成功,形成通路后,再通过该通道进行数据的传输。
1)创建Socket服务,并指定要连接的主机端口。通路一建立,就会产生Socket流(包括输入流和输出流),通过方法获取
2)为了发送数据,应获取Socket中的输出流,如果要接收服务端的反馈信息,还需要获取Socket的输入流
3)通过输出流的write()方法将要发送的数据写入到流中
4)关闭Socket流资源
服务端:
服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。需监听一个端口。
1)建立服务端的Socket服务,并监听一个端口。通过ServerSocet带端口参数的构造函数
2)获取连接过来的客户对象,通过ServerSocket的accept()方法,此方法是阻塞式的,如果服务端没有连接到就会一直等待
3)客户端如果发过来数据,则服务端要使用对应的客户端对象,并获取到该客户端对象的读取流读取发过来的数据,并输出到指定目的地。
4)关闭服务端(可选)。一般服务端是常开的,因为在实际应用中,随时有客户端在请求连接和服务。但这里需要定时关闭客户端对象流,避免某一个客户端长时间占用服务器端。
示例:
练习二、/*TCP传输*/ /* 客户端: 1、创建Socket服务,并指定要连接的服务器的Ip和端口号 2、向服务端发送数据 3、关闭资源 */ import java.net.*; import java.io.*; class SocketDemo { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10000); OutputStream out = s.getOutputStream(); out.write("TCP come by me".getBytes()); s.close(); } } /* 服务端: 1、创建Socket服务,并指定端口号 2、使用accept方法得到Socket连接过来的客户端对象 3、通过客户端对象获取客户端发送过来的数据,并打印在控制台 4、关闭资源(可选) */ class ServerSocketDemo { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10000); Socket s = ss.accept(); InputStream in = s.getInputStream(); byte [] data = new byte[1024]; int len = in.read(data); //打印客户端发送的数据 System.out.println(new String(data,0,len)); //打印客户端信息IP System.out.println(s.getInetAddress().getHostAddress()); } }
练习三、/*TCP传输*/ /* 客户端: 1、创建Socket服务,并指定要连接的服务器的Ip和端口号 2、向服务端发送数据 3、关闭资源 */ import java.net.*; import java.io.*; class SocketDemo { public static void main(String[] args) throws Exception { //创建socket服务,并指定要连接的服务器的IP和端口号 Socket s = new Socket("127.0.0.1",10000); //通过socket服务的getOutputStream方法获取输出流 OutputStream out = s.getOutputStream(); //向服务端传输数据 out.write("TCP 我来了!!!".getBytes()); //通过socket服务的getInputStream方法获取读取流,得到服务端反馈的信息 InputStream in = s.getInputStream(); byte [] data = new byte[1024]; //读取服务端反馈的信息 int len = in.read(data); System.out.println(new String(data,0,len)); s.close(); } } /* 服务端: 1、创建Socket服务,并指定端口号 2、使用accept方法得到Socket连接过来的客户端对象 3、通过客户端对象获取客户端发送过来的数据,并打印在控制台 4、关闭资源(可选) */ class ServerSocketDemo { public static void main(String[] args) throws Exception { //创建socket服务,并指定端口号 ServerSocket ss = new ServerSocket(10000); //通过accept方法获取socket连接的客户端对象 Socket s = ss.accept(); //通过获取读取流得到客户端传输过来的数据 InputStream in = s.getInputStream(); byte [] data = new byte[1024]; //读取客户端传输的数据 int len = in.read(data); //打印客户端发送的数据 System.out.println(new String(data,0,len)); //打印客户端信息IP System.out.println(s.getInetAddress().getHostAddress()); //向客户端反馈信息 OutputStream out = s.getOutputStream(); out.write("已收到!!!".getBytes()); } }
练习四、/* 练习 需求:建立一个文本转换服务器 客户端给服务端发送文本,服务端会将文本转成大写再返回给客户端。 而且客户端可以不断的进行文本转换。当客户端输入over时,转换结束。 分析: 客户端: 既然是操作设备上的数据,那么就可以使用io技术,并按照io的操作规律来思考。 源:键盘录入 目的:网络设备,网络输出流。 而且操作的是文本数据。可以选择字符流。 步骤: 1、建立服务 2、获取键盘录入 3、将数据发给服务端 4、获取服务端返回的大写数据 5、结束,管资源。 都是文本数据,可以使用字符流进行操作,同时提高效率,加入缓冲。 此练习出现的问题: 现象:客户端和服务端都在莫名的等待。 原因:因为客户端和服务端都有阻塞式方法。这些方法没有读到结束标记。那么就一直等。而导致两端都在等待。 解决:需要用到刷新和换行的方式将写入和读取的数据从流中刷新到内存中 方式一:可用高效缓冲区类的newLine()换行作为结束标记,并用flush()进行刷新。 方式二:可用PrintWriter(s.getOutputStrean(),true)创建输出流对象,true作用是刷新,通过打印方法println()换行,“ln”表示换行。 */ import java.io.*; import java.net.*; class TransSocket { public static void main(String[] args) throws Exception { //创建Socket服务,并指定要连接的主机的IP和端口号 Socket s = new Socket("127.0.0.1",10002); //创建读取流对象,用于读取键盘录入,这里使用字符读取流 BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //定义目的,将数据写入到Socket输出流。发给服务端 // BufferedWriter bufw = // new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); //------------使用PrintWriter替代BufferedWriter-------- PrintWriter print = new PrintWriter(new BufferedWriter( new OutputStreamWriter(s.getOutputStream())),true); //定义读取流,读取服务端反馈的信息 BufferedReader readerServer = new BufferedReader(new InputStreamReader(s.getInputStream())); //读取键盘录入 String data = null; while((data=bufr.readLine())!=null){ //如果输入over转换结束 if("over".equals(data)) break; //-----使用打印流的特有方法替代缓冲------ // bufw.write(data); // bufw.newLine(); // bufw.flush(); print.println(data); //每发送一次,获取一次服务端反馈回来的信息 System.out.println("服务端反馈信息:"+readerServer.readLine()); } s.close(); bufr.close(); } } class TransServer { public static void main(String [] args) throws Exception { //创建ServerSocket服务,并指定端口号 ServerSocket ss = new ServerSocket(10002); //通过accept方法获取客户端对象 Socket s = ss.accept(); //显示客户端IP信息 System.out.println(s.getInetAddress().getHostAddress()); //获取客户端传输的数据 BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream())); //将客户端发送的文本转换为大写,反馈给客户端 // BufferedWriter bufw = // new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); //--------------使用打印流 替换 BufferedWriter-------- PrintWriter print = new PrintWriter(new BufferedWriter( new OutputStreamWriter(s.getOutputStream())),true); String line = null; while((line=bufr.readLine())!=null){ // bufw.write(line.toUpperCase()); // bufw.newLine(); // bufw.flush(); //--------------使用打印流的特有方法 替换 BufferedWriter的方法-------- print.println(line.toUpperCase()); } } }
/* 需求:向服务器上传一个文件,服务返回一条信息 1、客户端: 源:硬盘上的文件;目的:网络设备,即网络输出流。 若操作的是文本数据,可选字符流,并加入高效缓冲区。若是媒体文件,用字节流。 2、服务端: 源:socket读取流;目的:socket输出流。 3、出现的问题: 现象: a、文件已经上传成功了,但是没有得到服务端的反馈信息。 b、即使得到反馈信息,但得到的是null,而不是“上传成功”的信息 原因: a、因为客户端将数据发送完毕后,服务端仍然在等待着读取数据,并没有收到结束标记,就会一直等待读取。 b、上个问题解决后,收到的不是指定信息而是null,是因为服务端写入数据后,需要刷新,才能将信息反馈给客服端。 解决: 方法一:定义结束标记,先将结束标记发送给服务端,让服务端接收到结束标记,然后再发送上传的数据。但是这样定义可能会发生定义的标记和文件中的数据重复,而导致提前结束。 方法二:定义时间戳,由于时间是唯一的,在发送数据前,先获取时间,发送完后在结尾处写上相同的时间戳,在服务端,接收数据前先接收一个时间戳,然后在循环中判断时间戳以结束标记。 方法三:通过socket方法中的shutdownOutput(),关闭输入流资源,从而结束传输流,以给定结束标记。通常用这个方法。 */ import java.io.*; import java.net.*; class TextSocket { public static void main(String[] args) throws Exception { //创建socket服务 Socket s = new Socket("127.0.0.1",10003); //创建文件对象 File file = new File("D:\\file.txt"); //判断文件是否存在 if(file.exists()){ //创建本地读取文件流 BufferedReader bufr = new BufferedReader(new FileReader(file)); //定义目的,将数据写入到socket流,并传输个服务端 PrintWriter print = new PrintWriter(new BufferedWriter( new OutputStreamWriter(s.getOutputStream())),true); //定义读取流,读取服务端反馈信息 BufferedReader readerSer = new BufferedReader(new InputStreamReader(s.getInputStream())); String data = null; while((data=bufr.readLine())!=null){ //向服务端输出信息 print.println(data); } //使用socket服务的shutdownOutput方法,标识客户端数据传输已结束 s.shutdownOutput(); //打印服务端反馈信息 System.out.println(readerSer.readLine()); bufr.close(); s.close(); } else { System.out.println("该文件不存在"); s.close(); } } } class TextServer { public static void main(String [] args)throws Exception { //创建服务 ServerSocket ss = new ServerSocket(10003); //获取客户端对象 Socket s = ss.accept(); if(s!=null){ //打印客户端IP System.out.println("IP="+s.getInetAddress().getHostAddress()); //创建流对象,冲socket流中读取客户端传输的数据 BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream())); //创建输出流,用于给客户端反馈信息 PrintWriter printSocket = new PrintWriter(new BufferedWriter( new OutputStreamWriter(s.getOutputStream())),true); //创建输出流,将客户端传输的数据打印到本地硬盘 PrintWriter printText = new PrintWriter(new BufferedWriter( new FileWriter("d:\\server.txt")),true); //读取客户端传输的数据 String data = null; while((data=bufr.readLine())!=null){ printText.println(data); } printSocket.println("上传成功!"); printText.close(); bufr.close(); ss.close(); } else{ System.out.println("没有客户端访问!!!"); ss.close(); } } }
重点
一、用TCP客户端并发上传图片
1、一对一(单线程)上传的思路:
客户端
a、服务端点。
b、读取客户端已有的图片数据
c、通过Socket输出流将数据发给服务端
d、读取服务端反馈信息。
e、关闭
服务端
a、服务端服务,并监听窗口
b、获取客户端对象,并获取客户ip
c、读取客户端输入流数据
d、写入文件
e、用客户端输出流反馈信息
f、关流
2、单线程的服务端有个局限性。当A客户端连接上以后,被服务端获取到。服务端执行具体流程。这时B客户端连接,只能等待。因为服务端还没有处理完A客户端的请求。还没有循环回来执行下一次accept方法。所以,暂时获取不到B客户端对象。
那么为了可以让多个客户端同时并发访问服务端。服务端最好就是将每个客户端封装到一个单独的线程中,这样,就可以同时处理多个客户端请求。
如何定义线程呢?
只要明确了每一个客户端要在服务端执行的代码,将该代码存入run方法即可。
代码:
/*
需求:并发上传图片
*/
import java.io.*;
import java.net.*;
class PicSocket
{
public static void main(String[] args) throws Exception
{
if(args.length!=1){
System.out.println("请选择单个文件上传");
return;
}
//创建socket服务
Socket s = new Socket("127.0.0.1",1005);
//从控制台接收一个文件路径
File file = new File(args[0]);
if(!(file.exists()&&file.isFile)){
System.out.println("您上传的文件可能不存在");
return;
}
if(file.length=>1024*1024*5){
System.out.println("您上传到文件不能超过5M");
return;
}
if(!(file.getName().endsWith(".jpg"))){
System.out.println("您只能上传.jpg文件');
}
//创建本地读取流,关联文件
BufferedInputStream bufi =
new BufferedInputStream(new FileInputStream(file));
//定义目的,将数据写入socket流,并传输到客户端
BufferedOutputStream bufo =
new BufferedOutputStream(s.getOutputStream());
//定义读取服务端反馈信息流
BufferedReader bufr =
new BufferedReader(new InputStreamReader(s.getInputStream()));
//开始读取数据
int len = 0;
while((len=bufi.read())!=-1){
bufo.write(len);
}
s.shutdownOutput();
System.out.println(bufr.readLine());
s.close();
bufi.close();
}
}
class ServerThread implements Runnable
{
//ServetSocket服务对象
private Socket s;
public ServerThread(Socket s){
this.s = s;
}
public void run(){
int count =1;
try
{
//获取客户端对象
String ip = s.getInetAddress().getHostAddress();
File file = new File("d:\\pic");
if(!file.exists())
file.mkdir();
File dir = new File(file,ip+"("+(count)+")"+".jpg");
//创建读取流,从socket流中读取数据
BufferedInputStream bufr =
new BufferedInputStream(s.getInputStream());
//判断文件是否存在,存在更改文件名
while(dir.exists()){
dir = new File(file,ip+"("+(count++)+")"+".jpg");
}
//创建输出流对象,将客户端传输的数据保存到本地硬盘
BufferedOutputStream bufo =
new BufferedOutputStream(new FileOutputStream(dir));
int len = 0;
while((len=bufr.read())!=-1){
bufo.write(len);
}
//创建反馈信息流
PrintWriter print =
new PrintWriter(s.getOutputStream(),true);
print.println("上传成功!!!");
bufo.close();
}
catch (Exception e)
{
}
}
}
class PicServer
{
public static void main(String [] args) throws Exception
{
ServerSocket ss = new ServerSocket(1005);
while(true){
Socket s = ss.accept();
new Thread(new ServerThread(s)).start();
}
}
}
二、客户端并发登录
客户端通过键盘录入用户名,服务端对这个用户名进行校验。
如果该用户存在,在服务端显示xxx,已登陆;并在客户端显示xxx,欢迎光临。
如果用户不存在,在服务端显示xxx,尝试登陆;并在客户端显示xxx,该用户不存在。
最多就登录三次。
/* 需求:客户端并发登录 客户端通过键盘录入用户名,服务端对这个用户名进行校验。 如果该用户存在,在服务端显示xxx,已登陆;并在客户端显示xxx,欢迎光临。 如果用户不存在,在服务端显示xxx,尝试登陆;并在客户端显示xxx,该用户不存在。 最多就登录三次。 */ import java.io.*; import java.net.*; class UserSocket { public static void main(String[] args) throws Exception { //创建Socket服务 Socket s = new Socket("127.0.0.1",1006); //创建读取流,从键盘读取录入数据 BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //定义目的,将数据写入socket流,并传输到服务端 PrintWriter print = new PrintWriter(new BufferedWriter( new OutputStreamWriter(s.getOutputStream())),true); //创建读取服务端反馈信息流 BufferedReader readSer = new BufferedReader(new InputStreamReader(s.getInputStream())); String data = null; //控制用户输入次数 for(int i=0;i<3;i++){ data = bufr.readLine(); print.println(data); //打印服务端反馈信息 String str = readSer.readLine(); System.out.println(str); if(str.contains("欢迎")) break; } bufr.close(); s.close(); } } class User implements Runnable { private Socket s; public User(Socket s){ this.s = s; } public void run(){ try { System.out.println("IP"+(s.getInetAddress().getHostAddress())); System.out.println("22222222222"); //创建读取流,从socket流中读取客户端传输的数据 BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream())); //创建读取流,从本地硬盘读取用户信息 BufferedReader bufUser = new BufferedReader(new FileReader("D:\\userInfo.txt")); //定义打印流,输出服务端反馈信息 PrintWriter print = new PrintWriter(s.getOutputStream(),true); //这个for循环很重要,没有它,数据只能传输一次 /*这里一定要放到循环中来,因为这是一个线程,如果没有循环,服务端接收完 一次客户端传输的数据后,该线程就结束了,和之前的控制台转换大写是一个道理 */ for(int i=0;i<3;i++){ String name = bufr.readLine(); //定义标记,用于判断用户是否存在 boolean flag = false; String line = null; while((line=bufUser.readLine())!=null){ if(line.equals(name)){ flag = true; break; } } if(flag==true){ print.println(name+"欢迎光临!"); System.out.println(name+"已登陆"); break; } else{ print.println(name+"该用户不存在"); System.out.println(name+"尝试登陆"); } } } catch (Exception e) { } } } class UserServer { public static void main(String [] args)throws Exception { ServerSocket ss = new ServerSocket(1006); while(true){ Socket s = ss.accept(); System.out.println("1111111111111"); new Thread(new User(s)).start(); } } }
浏览器是一个标准的客户端,它可以对服务端传送过来的数据消息进行解析,把符合应用层协议的消息部分解析后,将头信息拆包掉,传送到应用层,只保留了正确的正文主题部分显示在主体部分上。
而由于使用java编译是在传输层和网际层处理的,所以,会接受到全部的消息,包含了头消息。而浏览器处于应用层,已将发送来的头消息去除,只留下了主体信息。
示例:
自定义服务器,用浏览器访问:
/* 客户端为浏览器 向浏览器反馈信息 注意:使用浏览器作为客户端访问服务器时,客户端会给服务端发送“消息头” 只需要通过socket服务的getInputStream()方法即可获取: 如:在客户端输入http://192.168.1.254:11000/myweb/demo.html HTTP/1.1 则客户端向服务端发送的消息头为: GET /myweb/demo.html HTTP/1.1 //这是客户端在告诉服务端要访问的资源路径 Accept: application/x-shockwave-flash, image/gif, image/x-xbitmap, image/jpeg, i mage/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application /msword, application/QVOD, application/QVOD,//这是在告诉服务端客户端支持的类型 Accept-Language: zh-cn //这是在告诉服务端客户端支持的语言 Accept-Encoding: gzip, deflate //这是在告诉服务端客户端支持的压缩格式 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0 .50727) Host: 192.168.1.254:11000 //这是告诉服务端客户端要访问的192.168.1.254主机的11000端口号 Connection: Keep-Alive //这是告诉服务端要保持连接 */ import java.net.*; import java.io.*; class ServerToIE { //在客户端输入http://127.0.0.1访问 public static void main(String[] args) throws Exception { //创建socket服务 ServerSocket ss = new ServerSocket(10086); //获取客户端对象 Socket s = ss.accept(); //获取客户端发送的数据(包含消息头) InputStream in = s.getInputStream(); byte [] data = new byte[1024]; int len = in.read(data); System.out.println(new String(data,0,len)); /*服务端反馈信息(包含应答消息头),不在浏览器上显示, 因为浏览器是应用层,被解析了,所以我们在浏览器上只能看到“哥们,收到!!”*/ PrintWriter print = new PrintWriter(s.getOutputStream(),true); print.println("哥们,收到!!!"); s.close(); } } //向服务端z(Tomcat服务器)发送自定义消息头,并打印应答消息头 class SocketToServer { public static void main(String [] args)throws Exception { //创建socket服务 Socket s = new Socket("127.0.0.1",10086); //定义目的,将数据写入Socket流,并传输给服务端 PrintWriter print = new PrintWriter(s.getOutputStream(),true); print.println("GET / HTTP/1.1"); /*这里我们是直接访问10086端口,要想指定资源路径, 可以使用Tomcat服务器,把资源放到Tomcat的webapps目录下即可*/ print.println("Accept: */*"); // */*代表所支持的类型及格式 如图片,视频及其格式 print.println("Accept-Language: zh-cn"); //支持的语言 print.println("Host: 127.0.0.1:10086 ");//要访问的主机的端口 print.println("Connection: closed"); //这里一般我们自己测试是使用closed //注意:这里一定要输出空行 print.println(); print.println(); //获取服务端反馈信息(包含应答消息头) BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream())); String line = null; while((line=bufr.readLine())!=null){ System.out.println(line); } s.close(); } }
四、URL和URLConnection
1、URL:
URI:范围更大,条形码也包含于此范围
URL:范围较小,即域名
方法:
1)构造函数:URL(String protocol,String host,int port,String file);//根据指定 protocol、host、port号和 file创建 URL对象。
2)String getProtocol();//获取协议名称
3)String getHost();//获取主机名
4)int getPort();//获取端口号
5)String getFile();//获取URL文件名
6)String getPath();//获取此URL的路径部分
7)String getQuery();//获取此URL的查询部,客户端传输的特定信息
注:一般输入网址,是不带端口号的,此时可进行获取,通过获取网址返回的port,若port为-1,则分配一个默认的80端口,如
int port = getPort();
if(port == -1)
port = 80;
2、URLConnection
方法:
1)URLConnection openConnection();//用URL调用此方法,返回一个 URLConnection 对象,它表示到 URL 所引用的远程对象的连接。
2)InputStream getInputStream();//获取输入流
3)OutputStream getOutputStream();//获取输出流
示例:
/*
自定义浏览器,显示网页信息
*/
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
class MyIEGUIDemo
{
//定义所需组件引用
private Frame f;
private Button but,bok;
private TextField tf;
private TextArea ta;
//构造函数
MyIEGUIDemo()
{
init();
}
//窗体基本设置于功能实现
public void init()
{
//组件实例化
f=new Frame("我的Window");
but=new Button("跳转");
tf=new TextField(50);
ta=new TextArea(25,60);
//基本设置
f.setBounds(300,150,500,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);
}
});
//“跳转”按钮事件
but.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
showFile();//显示网页内容在文本区中
}
});
//文本框键盘事件
tf.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
//如果键盘按下Enter键,就将网页内容显示在文本区中
if(e.getKeyCode()==KeyEvent.VK_ENTER)
showFile();
}
});
}
//显示网页内容
private void showFile()
{
ta.setText("");
String path=tf.getText();//获取输入的路径
try
{
//封装地址对象
URL url =new URL(path);
//连接网页服务器,URLConnection是一个抽象类,里面包含getInputStream方法和getOutputStream方法
URLConnection conn=url.openConnection();
//读取流,用于读取服务器返回数据
InputStream in=conn.getInputStream();
byte[] buf=new byte[1024*1024];
int len=in.read(buf);
//将数据显示在文本区中
ta.append(new String(buf,0,len));
}
catch (Exception e)
{
throw new RuntimeException("连接"+path+"网站失败");
}
}
public static void main(String[] args)
{
//运行窗体
new MyIEGUIDemo();
}
}
补充
1、Socket类的构造函数中,有一个空参数的构造函数:
Socket()//通过系统默认类型的 SocketImpl创建未连接套接字对象
可以通过connect(SocketAddress endpoint)方法来连接服务器。而SocketAddress是一个抽象类,它的子类InetSocketAddress实现了IP套接字地址(IP地址+端口号)。所以就可以连接到服务器了。
2、ServerSocket对象中的构造函数:
ServerSocket(int port,int backlog),其中的backlog表示队列的最大长度,即最多连入客户端的个数,即最大连接数。
3、在浏览器输入网址访问一台主机所做的操作:
如输入http://61.135.169.125,可以直接连接此ip的主机,而我们一般是输入主机名:http:/www.baidu.ocm(百度主机对应的ip地址就是:61.135.169.125),那么此时浏览器做了神马操作呢?
也就是说如何通过主机名获取IP地址,从而连接到这台主机呢?这就需要将主机名翻译成IP地址,即域名解析:DNS
在进行访问的时候,会先在本地的hosts文件(c:\windows\system32\drivers\ext\host)中找对应的映射。若有,则直接返回请求;若无,则到公网的映射列表即DNS中找对应的映射,找到后,将主机名对应的IP地址返回给本机,本机通过这个IP地址找到对应的服务器。
示意图:
host应用:可屏蔽一些恶意网址,即将对应的映射关系写入hosts中,将IP地址改为本机的回环地址,那么会直接找到hosts,就不会将请求发送出去了。