黑马程序员——网络编程3:网络通讯组件介绍及演示-下

来源:互联网 发布:澳洲gpa算法 编辑:程序博客网 时间:2024/05/14 17:40
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------


2.3  TCP传输协议套接字——ServerSocket & Socket(接上篇)

(4)  TCP传输服务演示实例(接上篇)

需求5:通过TCP传输服务,实现客户端向服务端发送图片的功能。

思路:基本的服务端/客户端Socket服务建立与网络连接过程都是一样的,不再赘述。所不同的是,本例中操作的将不再是文本文件,因此不必再使用转换流,直接通过Socket读写流即可完成需求。若需要提高读写效率,还可将Socket读写流通过缓冲字节流进行封装。由于涉及到字节缓冲,因此发送数据时,一定要进行刷新动作。

代码12:

import java.io.*;import java.net.*; class TcpClient{public static void main(String[] args) throws IOException{Socket clientSocket = new Socket(InetAddress.getLocalHost(), 10002); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream();             byte[] buf = new byte[1024];int len = in.read(buf);System.out.println(newString(buf, 0, len)); File pic = new File("E:\\Tulips.jpg");if(!pic.exists())pic.createNewFile(); //向服务端发送文件名out.write(pic.getName().getBytes());             //创建字节文件读取流,并与将要被传输的图片文件关联起来BufferedInputStream bis =new BufferedInputStream(new FileInputStream(pic));//将Socket写入流通过缓冲字节写入流封装起来,向服务端发送数据BufferedOutputStream bos =new BufferedOutputStream(out); while((len = bis.read(buf)) != -1){bos.write(buf,0, len);bos.flush();//一定要进行刷新动作} //关闭Socket写入流,向服务端Socket读取流发送结束标记clientSocket.shutdownOutput(); len = in.read(buf);System.out.println(newString(buf, 0, len));             bis.close();clientSocket.close();}}class TcpServer{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(10002); Socket clientSocekt = serverSocket.accept();System.out.println(clientSocekt.getInetAddress().getHostAddress()+"...connected."); InputStream in = clientSocekt.getInputStream();OutputStream out = clientSocekt.getOutputStream(); out.write("Connectedsuccessfully.".getBytes()); //读取客户端发送的文件名byte[] buf = new byte[1024];int len = in.read(buf);String[] temp = new String(buf, 0, len).split("\\.");String fileName = temp[0] + "(2)." + temp[1]; File pic = new File("E:\\", fileName);if(!pic.exists())pic.createNewFile(); //将Socket读取流通过缓冲字节读取流封装起来,用于接收来自服务端的数据BufferedInputStreambis =new BufferedInputStream(in);//创建一个缓冲字节写入流,并与以客户端发送的文件名命名的文件关联起来BufferedOutputStream bos =new BufferedOutputStream(new FileOutputStream(pic)); while((len= bis.read(buf)) != -1){bos.write(buf,0, len);bos.flush();//一定要进行刷新动作} out.write("UpLoadsuccess.".getBytes()); bos.close();clientSocekt.close();}}
执行以上代码,即可完成客户端向服务端发送图片文件的需求。

 

需求6:在需求5的基础上,实现多个客户端并发向服务端发送图片。

思路:

        若要实现多个客户端并发访问服务端,也就是说,服务端同时处理多个客户端的连接请求以及数据传输服务,靠单线程是不行的。因为按照单线程的特点,即使在代码12的基础上,在服务端代码中添加while(true)无限循环,通过accept方法不断接受客户端连接也不能解决该需求。因为在一个循环内,只能处理一个客户端的数据传输操作,只有处理完该客户端才能继续操作间下一个客户端的数据请求,以此类推,这样就会造成其他客户端等待时间过长。

        因此,可以使用多线程技术解决这个问题。基本思想就是,服务端开启while(true)无限循环,只要accept方法接收到客户端Socket服务对象,就开启一个单独的子线程,并在这个子线程内部完成对客户端的数据传输服务。在处理前一个客户端的请求的同时,如果又有客户端连接到服务端,就再开启一个子线程处理该客户端的请求,以此类推。当然服务器的处理能力是有限的,通常多会限制客户端同时连接数量。那么当同时连接到服务端的客户端数量达到这个数量后,服务端就应该暂时停止与新的客户端建立连接,直到有客户单断开连接为止。

        为了应用多线程技术,首先需要定义一个实现了Runnable接口的类,并通过该类构造方法传入通过accept方法接收的客户端Socket对象。而在run方法中定义的就是服务端处理客户端数据请求的代码,包括发送连接状况以及数据传输状况信息代码,以及数据传输代码等等。

代码13:

import java.io.*;import java.net.*; class TcpClient{public static void main(String[] args) throws IOException{Socket clientSocket = new Socket(InetAddress.getLocalHost(), 10002); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream();             byte[] buf = new byte[1024];int len = in.read(buf);System.out.println(newString(buf, 0, len)); File pic = new File("E:\\Tulips.jpg");if(!pic.exists())pic.createNewFile(); //向服务端发送文件名out.write(pic.getName().getBytes());             //创建字节文件读取流,并与将要被传输的图片文件关联起来BufferedInputStream bis =new BufferedInputStream(new FileInputStream(pic));//将Socket写入流通过缓冲字节写入流封装起来,向服务端发送数据BufferedOutputStream bos =new BufferedOutputStream(out); while((len = bis.read(buf)) != -1){bos.write(buf,0, len);bos.flush();} //关闭Socket写入流,向服务端Socket读取流发送结束标记clientSocket.shutdownOutput(); len = in.read(buf);System.out.println(newString(buf, 0, len));             bis.close();clientSocket.close();}}//线程类class DataTransThread implements Runnable{private Socket clientSocket;DataTransThread(Socket clientSocket){this.clientSocket = clientSocket;} /*由于run方法不能声明抛出异常因此对于内部可能抛出的异常必须逐个进行try-catch处理*/public void run(){System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected."); InputStream in = null;OutputStream out = null;try{in = clientSocket.getInputStream();out = clientSocket.getOutputStream();}catch(IOException e){throw new RuntimeException("获取客户端Socket读写流失败!");} try{out.write("Connectedsuccessfully.".getBytes());}catch(IOExceptione){throw new RuntimeException("向客户端发送连接信息失败!");} //读取客户端发送的文件名byte[] buf = new byte[1024];int len = 0;try{len = in.read(buf);}catch(IOExceptione){throw new RuntimeException("接收客户端发送的文件名失败!");}             /*为防止上传同名文件,而造成文件覆盖通过计数器变量,为文件名按照顺序添加序号*/              int count = 1;String fileName = new String(buf, 0, len);File pic = new File("E:\\", fileName); String[] temp = null;String newFileName = null;while(pic.exists()){temp = fileName.split("\\.");newFileName = temp[0] +"("+ (count++) +")."+ temp[1];pic = new File("E:\\", newFileName);}try{if(!pic.exists())pic.createNewFile();}catch(IOExceptione){throw new RuntimeException("文件创建失败!");} //将Socket读取流通过缓冲字节读取流封装起来,用于接收来自服务端的数据BufferedInputStream bis =new BufferedInputStream(in);//创建一个缓冲字节写入流,并与以客户端发送的文件名命名的文件关联起来BufferedOutputStream bos = null;try{bos = new BufferedOutputStream(new FileOutputStream(pic));}catch(FileNotFoundException e){throw new RuntimeException("指定文件不存在,或指定路径错误!");}             try{while((len= bis.read(buf)) != -1){bos.write(buf,0, len);bos.flush();} out.write("UpLoadsuccess.".getBytes());}catch(IOExceptione){throw new RuntimeException("数据传输失败!");}finally{try{if(bos != null)bos.close();}catch(IOExceptione){throw new RuntimeException("字节写入流关闭失败!");}                           try{clientSocket.close();}catch(IOExceptione){throw new RuntimeException("客户端Socket服务关闭失败!");}}}}class TcpServer{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(10002); while(true){//每连接一个Socket客户端//就为其单独开启一个线程,为其处理数据请求Socket clientSocekt = serverSocket.accept();new Thread(new DataTransThread(clientSocekt)).start();}}}
执行以上代码即可实现多个客户端同时向服务端发送图片文件的功能。

代码说明:

        (1)  以上代码中开启了while(true)无限循环来不断接受新的客户端,并与他们一一建立连接。在这一过程中,由于accept方法是阻塞式方法,因此不会造成无限循环而耗费大量系统资源。

        (2)  由于run方法不能声明抛出异常,因此需要为其中可能会抛出异常的代码逐个进行try-catch处理。

        (3)  由于服务端可以接受多个客户端发送的文件,为了区分同名文件,避免文件覆盖,按照创建顺序在原文件名后添加序号。这一部分代码的原理为:首先创建具有原始文件名的文件对象。然后通过while循环判断该文件是否存在。如果存在,则满足条件执行循环内部代码,将“1号”添加至文件名中再次创建文件对象,同时令计数器变量自增;重复以上过程,直到包含某个序号的文件不存在时,跳出循环,此时则创建了包含新序号的不重复文件名文件。

 

需求7:实现多个客户端并发登陆至服务端的功能。要求客户通过键盘录入用户名,并通过客户端程序将用户名发送至服务端。如果该用户名存在,则服务端允许该用户登陆,并在服务端显示“用户名,已登录”,并向客户端发送登陆成功信息;如果该用户名不存在,则在服务端显示“用户名,尝试登陆”,并向客户端发送登陆失败信息。服务端最多允许客户端尝试登陆3次。

思路:

        首先要有一个存有全部用户的用户名列表,因此这里我们创建一个用户名列表文本文件,将所有用户名事先记录到到这一文件中。当有客户端请求登录时,服务端将接收到的用户名与用户名列表文件中的用户名进行比对。

        客户端方面:由于需求中,限定了登陆次数。因此,通过for循环向服务端发送用户名。循环体内,通过键盘录入获取到输入的用户名字符串后,首先对其进行非空判断。接着将其通过Socket写入流发送到服务端。而后调用Socket读取流的read方法,等待服务端的反馈信息,并判断该反馈信息是否是登陆成功信息:若登陆成功,则直接跳出登陆循环,进而关闭键盘录入和客户端Socket服务。若登陆失败,则进行下一轮登陆循环。

        服务端方面:由于要实现多个客户端的并发登陆,因此同样要使用多线程技术,将处理客户端请求代码定义在线程类run方法中。同样为了限制登陆次数,使用for语句开启登陆校验循环。循环体内,首先通过Socket读取流接收客户端发送的用户名字符串。接着,通过与用户名列表文件关联起来的字符读取流遍历用户名,并依次与客户端发送的用户名比较。若校验成功,则在服务端打印登陆成功信息,并将该信息发送给客户端;若验证失败,则在服务端打印登陆失败学信息,同样将该信息发送至客户端,并进行下一轮验证循环。

代码14:

import java.io.*;import java.net.*; class TcpClient{publicstatic void main(String[] args) throws IOException{Socket clientSocket = new Socket(InetAddress.getLocalHost(), 10005); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream(); byte[] buf = new byte[1024];int len = in.read(buf);System.out.println(newString(buf, 0, len)); //开启键盘录入,用于输入用户名BufferedReader keyboard =new BufferedReader(new InputStreamReader(System.in));/*将Socket读取流封装为缓冲字符读取流向服务端发送用户名的同时,接收客户端反馈的信息*/              BufferedReader bufr =new BufferedReader(new InputStreamReader(in));             PrintWriter pw =new PrintWriter(out, true); String username = null, info = null;/*由于只允许客户端尝试登陆3次因此通过for循环,将循环次数限定为3次*/for(int x = 0; x<3; x++){/*为防止向服务端发送空文本这里对发送内容进行非空判断*/username = keyboard.readLine();if(username == null)break; pw.println(username); /*接收服务端反馈的信息当该信息为登陆成功信息时跳出登陆循环*/info = bufr.readLine();System.out.println(info);if(info.contains("登陆成功"))break;} keyboard.close();clientSocket.close();}}//用户线程class UserThread implements Runnable{private Socket clientSocket;UserThread(Socket clientSocket){this.clientSocket = clientSocket;}public void run(){//为演示方便,不再对每个方法逐个进行try-catch处理try{System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected."); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream(); PrintWriter pw =new PrintWriter(out, true);pw.println("服务器连接成功。");                    BufferedReader bufr =new BufferedReader(new InputStreamReader(in)); //将用户名列表文件封装为一个File对象File users = new File("E:\\users.txt"); BufferedReader localBufr = null;String localUserame = null, sendedUsername = null;//登陆循环//为限制校验次数,因此校验循环只进行3次for(int x = 0; x<3; x++){/*每进行一次登陆循环都要重新创建一个与用户名列表文件关联起来的字符读取流*/localBufr = new BufferedReader(new FileReader(users));sendedUsername = bufr.readLine();                           //当客户端发送null时,直接结束登陆循环if(sendedUsername == null)break;                           /*定义标记,初始化值为false当校验成功以后,将其值置为true*/boolean flag = false;//校验循环while((localUserame = localBufr.readLine()) != null){if(localUserame.equals(sendedUsername)){flag = true;break;}} /*判断标记若为true,表示客户端发送用户名存在,则向客户端发送登陆成功信息,并跳出校验循环;若为false,表示客户端发送用户名不存在,则想客户端发送登陆失败信息,并进行下一轮校验循环*/if(flag){System.out.println(sendedUsername+",已登录。");pw.println(sendedUsername+",登陆成功!");break;}else{System.out.println(sendedUsername+",尝试登录。");pw.println(sendedUsername+",该用户名不存在,请重新输入!");}} localBufr.close();clientSocket.close();}catch(IOExceptione){throw new RuntimeException("数传传输失败!");}}}class TcpServer{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(10005); while(true){Socket clientSocket = serverSocket.accept();new Thread(new UserThread(clientSocket)).start();}}}
执行以上代码后,当客户端命令行显示以下内容时:

Abc,该用户名不存在,请重新输入!

haha

haha,该用户名不存在,请重新输入!

Kate

Kate,登陆成功!

服务端命令行显示内容为:

172.18.49.192...connected.

Abc,尝试登录。

haha,尝试登录。

Kate,已登录。

代码说明:

        (1)  客户端代码中,由于键盘录入不再通过while循环对输入内容进行非空判断,因此需要在for循环体内进行这一操作,因为向服务端发送“null”是没有意义的。当检测到键盘输入为“null”时,跳出循环结束登陆。在命令行界面下,当我们键入“Ctrl+C”人为停止程序运行时相当于输入了“null”。

        (2)  在客户端登陆循环内,向服务端发送键盘录入的用户名以后,应接收服务端的反馈信息,并对这一信息进行判断:若此信息为登陆成功则跳出循环,表示登陆成功。

        (3)  服务端for循环内,定义了一个变量名为“flag”的布尔类型变量,它的作用是判断登陆是否成功的标记,初始化值为false。当用户名校验成功时,并非直接向客户端发送登陆成功信息,而是将标记置为true。而登陆信息的发送操作是在while校验循环外执行的。当标记我true时表示登陆成功,反之表示登陆失败。

        之所以将登陆信息交互操作定义在while校验循环之外,是因为有多个用户名保存在了用户名列表文件中,因此即使客户端发送的用户名是存在的,校验循环可能也需要多次执行才能校验成功。因此若将登陆信息交互代码定义在while循环内,那么每校验失败一次就向客户端发送登陆失败信息,就会导致客户端过早结束了登陆循环。因此通过一个标记,将循环校验代码和登陆信息交互代码分割开来,以提高代码的健壮性。

        (4)  当服务端检测到客户端发送的用户名为null时(通常发生于强制结束用户端程序的执行时),表示客户端不再尝试登陆服务器,因此此时服务端将直接跳出登陆循环,结束该用户线程的执行。

        (5)  注意到服务端for登陆循环内,每执行一次循环都会创建一个新的与用户名列表文件关联起来的字符读取流。这么做的原因是,当第一次或第二次校验失败时,原先的字符读取流已经读取到了文件末尾,此时再调用其readLine方法,返回值必定是null,因此与客户端发送的用户名比较结果将为false,从而向客户端发送登陆失败信息,而实际上服务端此时并没有进行校验操作。因此每执行一次登陆循环都要重新创建字符读取流,从头开始遍历用户名。

(5)  TCP传输服务实现浏览器访问服务器

        在日常生活中我们最常见的客户端,其实就是各式各样的浏览器。当使用浏览器去访问各个网站时,实际就是在通过浏览器这个客户端去访问目标网站所在的服务器,并与之进行实时的数据传输。因此这一部分,我们将演示通过现有浏览器、或自定义浏览器,去访问Tomcat服务器以及自定义服务器等功能,基本的TCP传输服务代码与前述都是一致的。

需求1:自定义服务端程序,并通过现有浏览器对其进行访问,比如Chrome、搜狗浏览器、IE浏览器等等。

思路:由于该需求仅为演示,因此只要有客户端浏览器访问该服务端程序时,就向其反馈一段文本,表示连接成功。由于浏览器都内部本身包含了接收服务端发送的信息,并对其进行解析的功能,因此浏览器可自定显示服务端发送的信息。

代码15:

import java.io.*;import java.net.*; //用户线程类class UserThread implements Runnable{    private Socket clientSocket;UserThread(Socket clientSocket){this.clientSocket = clientSocket;}public void run(){try{System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected."); OutputStream out = clientSocket.getOutputStream();PrintWriter pw = new PrintWriter(out, true); //向客户端浏览器发送链接成功信息//包含了html代码pw.println("<fontcolor='green' size='7'>Server connected.</font>"); clientSocket.close(); }catch(IOExceptione){throw new RuntimeException("数据传输失败!");}}}class ServerDemo{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(10005); while(true){Socket clientSocket = serverSocket.accept();new Thread(new UserThread(clientSocket)).start();}}}
执行以上代码所代表的服务端程序以后,开启任意一种浏览器程序,并在地址栏中输入“localhost:10005”即表示访问本地的10005端口,与此同时,浏览器中将会显示“Server connected.”等字样。

代码说明:

        (1)  以上代码中服务端代码与前述客户端并发上传图片,以及客户端并发登陆服务端示例中的代码并没有太大区别,只不过客户端是现有浏览器软件,它的作用就是解析服务端发送的信息,并将这些信息显示在浏览器页面中。

        (2)  服务端发送的信息中包含了一部分html代码,用于调整字体颜色和大小,并没有实际意义,因此不会显示在浏览器页面中。如果浏览器具有查看页面源码的功能,即可知道服务端发送的信息就是“<font color='green' size='7'>Server connected.</font>”,只不过被浏览器解析,并按照html代码所表示含义调整了字体显示效果。

 

需求2:通过现有浏览器访问Tomcat服务器。Tomcat服务器是纯Java编写的服务器程序,其基本的服务器原理其实与我们前面所演示的服务其代码都是一样的,同样通过创建ServerSocket对象建立TCP协议服务端Socket服务。

        首先大家需要访问Tomcat服务器软件的官方网站“tomcat.apache.org/index.html”,并下载一个Tomcat服务器程序。下载完毕以后,双击打开bin目录下的startup.bat文件,即可弹出一个命令行窗口,并不断刷新大量的文本内容,此即表示服务端程序正在开启,如下图所示。


服务器开启完毕后,打开任意一种浏览器,并在地址栏中输入“localhost:8080”,即可显示Tomcat服务器主页面,如下图所示,


其中,localhost代表了本地主机名,而8080是Tomcat服务器程序的默认端口。当然,我们也可以通过Tomcat服务器,来显示自定义网页文件。将自定义的“.html”文件存储到Tomcat服务器软件根目录下的webapps文件夹中,然后通过浏览器访问该页面时,在端口号后输入网页文件地址以及网页文件名即可。比如,我们自定义了一个名为“demo.html”网页文件,并将其存储在webapps/myweb目录中,其中myweb文件夹为自定义文件夹,则需要在浏览器地址栏中输入“localhost:8080/myweb/deomo.html”即可。

 

需求3:自定义客户端浏览器程序,实现与Tomcat服务器连接,并与之进行数据传输。

        首先,我们需要了解浏览器在向服务器请求数据的时候,都向它发送了哪些信息,才能请求到想要访问的页面数据。为此,我们自定义一个服务端程序,然后通过Socket读取流获取现有浏览器客户端在向服务端请求数据时发送的信息,并将这些信息打印到控制台。服务端代码如下,

代码16:

import java.io.*;import java.net.*; class UserThread implements Runnable{private Socket clientSocket;UserThread(Socket clientSocket){this.clientSocket = clientSocket;}public void run(){try{System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected.");             InputStream in = clientSocket.getInputStream();OutputStream out =clientSocket.getOutputStream(); //获取浏览器发送的请求信息//并将该信息打印到控制台byte[] buf = new byte[1024];int len = in.read(buf);System.out.println(newString(buf, 0, len)); PrintWriter pw = new PrintWriter(out,true);pw.println("<fontcolor='green' size='7'>Welcome!</font>"); clientSocket.close(); }catch(IOException e){throw newRuntimeException("数据传输失败!");}}}class ServerDemo2{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(10006); while(true){Socket clientSocket =serverSocket.accept();new Thread(newUserThread(clientSocket)).start();}}}
执行以上服务端代码后,打开一个浏览器(我用的是Chrome),并在地址栏中输入“localhost:10006”,即可在页面中显示“Welcome!”等字样。同时,在命令行中同样显示了一些信息,如下所示:

0:0:0:0:0:0:0:1...connected.

GET /HTTP/1.1

Host:localhost:10006

Connection:keep-alive

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

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

Chrome/42.0.2311.135Safari/537.36

Accept-Encoding:gzip, deflate, sdch

Accept-Language: zh-CN,zh;q=0.8

 

以上信息就是,浏览器向服务端发送的请求消息头,而这些信息实际上体现的就是网络参考模型中应用层所使用的http协议,只有客户端和服务端均遵守这一协议才能在两者之间实现数据的交互。下面我们对部分信息的含义进行说明。

        GET:顾名思义,就是向服务端请求获取数据。第一个“/”后通常会跟一个文件路径,这个路径指向的就是浏览器客户端所请求的网页文件,比如需求2中自定义网页文件的路名为“/myweb/deomo.html”。那么当我们在浏览器地址栏中输入以下内容时:http://localhost:8080/myweb/demo.html

对应的GET请求信息将是:

GET /myweb/demo.htmlHTTP/1.1

其中,“/myweb”称为资源路径,也就是浏览器所要访问的资源在服务器中的路径。“demo.html”称为资源,也就是浏览器所要访问的网页文件。“HTTP/1.1”表示浏览器与服务端进行交互所使用的协议是HTTP,版本为1.1。

        Host:表示浏览器请求访问指定IP地址对应服务器的10006端口。之所要具体指明端口号是因为,虽然一台服务器对应一个IP地址,但是一台服务器中却可以安装多台主机,每个主机对应不同的端口号,因此必须要知名想要访问的主机端口号。

Accept:表示浏览器能够接收的数据文件格式。包括有,html、xhtml+xml、xml等格式的网页文件。当然根据浏览器的不同,能够接受的数据文件格式不同,比如说gif、jpeg、png等格式的图片,mpeg、quicktime格式的视频等等。

        Accept-Encoding:表示浏览器支持的封装形式。比如,像新浪这一类门户网站,每天的数据传输量都是非常巨大的,传输量越大占用的网络带宽就越大,对应的带宽费用就越高;并且,一次性将用户请求的数据发送给客户端,传输速度较慢。因此为了提高数据传输效率,首先在服务端对将要传输的数据进行压缩,减小数据的体积,而客户端接收到压缩数据后,再对其进行解压缩即可获得所需数据。因此这里指的封装形式,就代表了数据的压缩方式。当服务端使用浏览器支持的封装形式对数据进行压缩,客户端就可以对其进行解压。

        Accept-Language:表示浏览器能够接受的语言是简体中文。

        Connections:代表连接状态,keep-alive的意思是保持存活。这一信息的作用是告诉服务端,在发送完请求数据以后,是否继续与与服务端的保持连接。keep-alive表示继续连接,若这一项为closed,就表示发送完数据后,断开连接。

        注意到,请求消息头下面包含了一个空行,该空行实际是由两个回车/换行对实现的,也就是\r\n\r\n。通常空行后接请求数据体,比如html格式的网页文件。因此这里的空行就是用于区分请求消息头和请求数据体,体现了http协议的一个标准。以上这些请求消息头的内容并非一定要全部包括,他会根据不同的浏览器发送不同的消息内容,但最重要的资源路径和资源一定要发送给服务端。

        那么了解了浏览器通过http协议与服务端实现数据交互的方式以后。我们就可以效仿现有浏览器向服务端请求数据的方式,自定义一个浏览器程序向Tomcat服务器请求数据。

思路:按照常规方法建立客户端Socket服务以后,向服务端发送请求消息头。Tomcat服务器程序接收以上信息并对其进行解析以后,就会对这一请求进行响应,向客户端发送数据,此时客户端即可使用Socket读取流接收这些数据。

代码17:

import java.io.*;import java.net.*; class MyBrowser{public static void main(String[] args)throws IOException{Socket clientSocket = new Socket(InetAddress.getLocalHost(), 8080); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream(); //向服务端发送请求消息头PrintWriter pw = new PrintWriter(out, true); pw.println("GET/mywebs/demo.html HTTP/1.1");pw.println("Accept*/*");// */*表示支持所有数据文件类型pw.println("Accept-Language:zh-CN");pw.println("Host:localhost:8080");pw.println("Connection:Keep-alive");pw.println("\r\n");//一定要在请求消息头后面添加一个空行             //接收服务端发送的响应信息BufferedReader bufr =new BufferedReader(newInputStreamReader(in, "UTF-8")); String msg = null;while((msg = bufr.readLine()) !=null){System.out.println(msg);} clientSocket.close();}}
开启Tomcat服务器,并执行以上代码的结果为:

HTTP/1.1200 OK

Server:Apache-Coyote/1.1

Accept-Ranges:bytes

ETag:W/"236-1433576696545"

Last-Modified:Sat, 06 Jun 2015 07:44:56 GMT

Content-Type:text/html

Content-Length:236

Date:Sat, 06 Jun 2015 07:45:00 GMT

 

<html>

<body>

        <h1>主页</h1>

        <fonr size=5color=green>Welcome!</font>

        <div>

                内容</br>

                内容</br>

                内容</br>

        </div>

</body>

</html>

以上信息就是Tomcat服务器针对我们自定义浏览器发送的请求信息,所反馈的应答消息头和请求数据,两者之间同样以一个空行分割,符合http的协议标准。应答消息头与请求消息头在形式上非常相似:

        第一行指明了服务器所使用的协议为HTTP/1.1,后接值为200的响应码,表示请求成功,OK表示描述信息,同样代表请求成功。还有许多其他的常见响应码,比如大家常见的404,表示未能找到请求的页面等。

        Server表示服务端程序是由Apache提供的。

        Last-Modified代表了客户端所请求页面的最后修改时间。

        Content-Type表示服务端向客户端发送的文件类型为html格式的文本。

        Conten-Length:表示发送的页面文件大小为236字节。

        Date表示当前时间。

        由于客户端向服务端发送的请求消息头中Connections项的值为keep-alive,因此当服务端发送完以上数据以后,会继续保持与客户端之间的连接状态,但又并没有发送任何内容,因此客户端将处于等待状态,最终因连接超时而自动断开连接。如果将客户端发送的请求消息头中Connection项的值改为closed,则服务单发送完指定数据以后就会字段与客户端断开连接。

 

需求4:将需求3中的自定义浏览器程序封装为GUI形式。

思路:浏览器客户端与服务端连接的建立、以及向服务端发送请求消息头等操作与需求3是一样的,并且GUI的设置在GUI系列博客中详细介绍过,因此这里均不再赘述。而对于该需求最为重要的是对用户指定的URL的解析。

        所谓URL是Uniform Resource Locator的缩写,统一资源定位符。它的作用就是指定访问服务器的协议(如HTTP)、服务器的名称和文件在服务器上的位置。通常用户会将URL输入到浏览器的地址栏中,按下回车键,浏览器即可按照URL中的信息,与指定服务器建立连接,并按照指定协议的格式要求向服务端发送请求消息头,获取用户指定的服务器资源。

        换句话说,当用户向浏览器地址栏中输入“http://172.18.23.143:8080/mywebs/demo.html”时,如何将其转换为请求消息头,并发送给服务端是本需求的关键问题。由于此前这一操作是由浏览器自定完成的,因此自定义浏览器程序中就由我们来定义解析URL的方法。

代码18:

import java.io.*;import java.net.*;import java.awt.*;import java.awt.event.*; class MyFrame{private Frame f;private Button but;private TextArea ta;private TextField tf; MyFrame(){init();} public void init(){f = newFrame("MyBrowser");f.setBounds(450, 200, 600, 460);f.setLayout(new FlowLayout()); tf = new TextField(73);f.add(tf); but = new Button("转到");f.add(but); ta = new TextArea(24,79);f.add(ta); event(); f.setVisible(true);}private void event(){f.addWindowListener(new WindowAdapter(){public voidwindowClosing(WindowEvent e){System.exit(0);}});but.addActionListener(new ActionListener(){public voidactionPerformed(ActionEvent e){ta.setText("");                           work();}});tf.addKeyListener(newKeyAdapter(){public voidkeyPressed(KeyEvent e){if(e.getKeyCode() == KeyEvent.VK_ENTER){    ta.setText("");work();}}});ta.addKeyListener(newKeyAdapter(){public voidkeyPressed(KeyEvent e){e.consume();}});}private void work(){String[] resourceInfo = analysis(tf.getText());connection(resourceInfo);}//解析URLprivate String[] analysis(String url){String temp = url.substring(url.indexOf("//")+2);String address = temp.substring(0,temp.indexOf("/"));String[] ip_port = address.split(":");String resourcePath =temp.substring(temp.indexOf("/"), temp.length());             //将主机IP地址、端口号以及资源路径等信息封装成字符串数组return new String[]{ip_port[0],ip_port[1], resourcePath};}//通过URL中的服务器信息建立与服务端的连接//并发送请求消息头private void connection(String[] resourceInfo){try{Socket clientSocket = newSocket(resourceInfo[0],Integer.parseInt(resourceInfo[1])); InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream(); //向服务端发送请求消息头PrintWriter pw = newPrintWriter(out, true); pw.println("GET"+resourceInfo[2]+" HTTP/1.1");pw.println("Accept*/*");pw.println("Accept-Language:zh-CN");pw.println("Host:"+resourceInfo[0]+":"+resourceInfo[1]);pw.println("Connection:closed");pw.println("\r\n");                    BufferedReader bufr =new BufferedReader(new InputStreamReader(in, "UTF-8")); String msg = null;while((msg =bufr.readLine()) != null){ta.append(msg);ta.append("\r\n");} clientSocket.close();}catch(IOException e){throw new RuntimeException("数据传输失败!");}}}class MyBrowserByGUI{public static void main(String[] args){new MyFrame();}}
开启Tomcat服务器,并执行以上代码后的结果为:


地址栏中输入的就是主机IP地址、端口以及资源路径等信息,而下面的文本框中显示的就是服务端发送的响应消息头以及客户端请求的数据,显示的内容与需求4是一致的,只不过这里使用了GUI进行操作和显示。

代码说明:

        URL解析的过程,实际上就是将用户输入在文本区域内的URL字符串分割为IP地址、端口以及资源路径等信息。以http://localhost:8080/mywebs/demo.html为例。首先通过“//”将域名分割为两部分,取后一部分,也就是“localhost:8080/mywebs/demo.html”。再通过“/”将其分为地址部分和资源部分,分别为“localhost:8080”和“/mywebs/demo.html”。注意资源路径头部的“/”是不能省略的,否则将找不到指定页面资源,因此代码中我们使用了substring方法,而非split方法。

        虽然我们通过以上代码获取到了服务端向客户端浏览器发送的响应数据,但是并未能对这些数据解析,也就是说,无法像现有浏览器那样,将页面内容按照指定的方式(html代码)呈现出来。因此这也就是浏览器软件的强大之处,它们可以解析html代码,按照网页编写者的意图,将网页中的各种显示效果呈现在浏览器中。

        另外,相比于浏览器软件,我们自定义的浏览器程序除了请求的页面数据以外,还多显示了响应消息头,而这实际上就是传输层标识头。由于浏览器通常工作在应用层,因此数据从传输层传到应用层的时候,应该将传输层标识头拆掉,这也就是浏览器软件不显示这一部分信息的原因。

        实际上,我们并不需要手动完成上述URL解析以及消息头拆包等复杂操作,因为Java标准类库中为我们提供了专门用于实现以上操作的类,利用这些类即可同时提高代码编写效率,以及代码的灵活性,因此我们将在下面的内容中对其进行详细介绍。

(6)  空参数Socket构造函数使用方法

        在前文介绍Socket类时,曾经提到该类提供了一个空参数构造方法,也就是说,创建实例对象时,并没有指定需要连接的服务端IP地址,以及端口号等信息。因此Socket类还提供了connect以建立与指定服务端的连接。我们首先回顾一下,空参数Socket构造函数,以及connect方法及其重载形式。

        public Socket():通过系统默认类型的SocketImpl创建未连接套接字。

        public void connect(SocketAddress endpoint) throws IOException:将此套接字连接到服务器。

        public void connect(SocketAddress endpoint, int timeout) throws IOException:将此套接字连接到服务器,并指定一个超时值。超时值零被解释为无限超时。在建立连接或者发生错误之前,连接一直处于阻塞状态。

        connect方法的参数类型称为SocketAddress,顾名思义,该类代表的就是套接字地址,也就是说该类的实例对象中封装的是目标服务端IP地址及端口号。由于这个类是抽象类,因此真正应用于代码中的是其子类InetSocketAddress实例对象。

        InetSocketAddress类的API文档描述为:此类实现IP套接字地址(IP地址+端口号)。它还可以是一个对(主机名+端口号),在此情况下,将尝试解析主机名。由此可知,InetSocketAddress对象的主要作用就是将服务端IP地址和端口号封装为一个对象,调用Socket对象的connect方法,并传递InetSocketAddress对象即可与目标服务器连接。这实际上个和通过带参数(IP地址+端口)Socket构造方法建立客户端Socket服务端是一样的。

2.4  URL & URLConnection

        正如前文所述,用户在浏览器地址栏中输入的URL只有被浏览器解析以后,才能与指定服务器进行连接,并发送请求信息。按照面向对象的思想,Java中为我们提供了用于描述URL这一类事物的类——URL类。该类对外提供了许多方法,包括自动完成域名解析等操作,提高开发效率。

2.4.1  URL

(1)  URL简介

API文档:

        类URL代表一个统一资源定位符(Uniform Resource Identifiers),它是指向互联网“资源”的指针。资源可以使简单的文件或目录,也可以使对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。

构造方法:

        public URL(String spec) throws MalformedURLException:根据String表示形式创建URL对象。换句话说,该构造方法可以将用户指定的字符串形式的URL封装为URL对象。

        public URL(String protocol, String host, int port, String file) throws MalformedURLException:根据指定的protocol(协议)、host(IP地址)、port号(端口号)和file(资源路径和资源名称)创建URL对象。该构造方法将一个完成域名手动分割为各个部分来封装为URL对象。

        以上两个构造方法,均声明抛出MalformedURLException异常。在指定URL字符串中找不到任何合法协议,或者无法解析该URL字符串时,将抛出此异常。

方法:

        public String getFile():获取此URL的文件名。该方法返回了用户指定URL中的资源路径以及资源名称部分。

        public String getHost():获取此URL的主机名。

        public String getPath():获取此URL的路径部分。该方法的返回值与getFile是一致的。

        public int getPort():获取此URL的端口号。当域名中没有指定端口号的时候,该方法返回的默认端口号位-1。

        public String getProtocol():获取此URL的协议名称。

        public String getQuery():获取此URL的查询部分。

        public URLConnection openConnection() throws IOException:返回一个URLConnection对象,它表示到URL所引用的远程对象的连接。通过该方法,能够按照指定的域名自动与目标主机进行连接,我们将在后面的内容中详细介绍这一方法以及URLConnection类。

以上这些方法,就体现了URL对象对封装在其内部URL的解析,实际上它的底层原理同样是对字符串的分割操作。

(2)  URL应用演示

代码19:

import java.net.*;import java.io.*;import static java.lang.System.out; class URLDemo{public static void main(String[] args) throws IOException{//将字符串形式的URL封装为URL对象URL url = newURL("http://172.18.70.96:8080/mywebs/demo.html"); out.println("Protocol:"+url.getProtocol());//获取协议out.println("Host:"+url.getHost());//获取主机out.println("Port:"+url.getPort());//获取端口out.println("Path:"+url.getPath());//获取路径out.println("File:"+url.getFile());//获取文件out.println("Query:"+url.getQuery());//获取URL的查询部}}
以上代码的执行结果为:

Protocol:http

Host:172.18.70.96

Port:8080

Path: /mywebs/demo.html

File:/mywebs/demo.html

Query:null

        由于以上代码比较简单易于理解,不再对其进行详细说明,其中有两处值得注意:第一,从结果来看getPath()方法和getFile()是一样的;第二,getQuery()的返回值为null,那么它的作用到底是什么呢?为了说明这两个问题,我们先将以上URL修改如下,

http://172.18.70.96:8080/mywebs/demo.html?name=abc&age=30

然后再次执行代码19,执行结果为:

Protocol:http

Host:172.18.70.96

Port:8080

Path:/mywebs/demo.html

File:/mywebs/demo.html?name=abc&age=30

Query:name=abc&age=30

此时,执行结果就有了一些不同。getPath()方法执行结果保持不变,而getFile()方法的执行结果除了资源路径和资源名称以外还包含了“?”后面的两个参数键值对,这就是两者之间的区别。而getQuery()方法的返回结果就是URL中所包含的所有参数键值对,使用“&”符号将这些参数连接起来。这些参数称为查询字符串,如果需要向服务端发送的请求信息包含了一些参数信息,就需要在URL中添加查询字符串。

        此外,如果URL中没有指定端口号,getPort方法的返回值默认为-1,此时通常会将这一默认值再重新赋值为80,也就是Web应用的默认端口号。

        那么通常的做法就是,通过URL对象封装用户指定的URL字符串,然后通过它的各种方法,获取到域名中的各种信息,然后通过这些信息,建立客户端Socket服务,并向服务端发送请求信息。浏览器判断出用户指定的协议以后,就会使用特定协议的解析引擎对服务端发送的数据进行解析,使这些数据能够正确的显示。所谓解析,对于应答消息头来说,就是将数据从传输层传输至应用层的过程中,获取了应答消息头中的信息以后将其去掉,也就是前文所述数据拆包;对于html代码来说,就是将网页文本内容按照html代码的效果进行显示。

2.4.2  URLConnection

(1)  URLConnection简介

        根据前述URL类的openConnection方法的说明可知,只要调用了URL对象的这一方法,返回了URLConnection对象,即可建立与指定URL中主机的连接,因此无需再创建Socket对象。实际上,之所以URLConnection对象能够实现这一功能,正是因为其内部封装了一个Socket对象,并根据用户指定的URL自动完成了客户端Socket服务的建立,以及与指定服务端建立连接等操作。此外,由于URL对象能够对封装于其中的URL进行解析,因此URLConnection对象还能自动向服务端发送数据请求消息头,只需直接获取Socket读取流接收服务端反馈的数据即可。并且由于用户指定的URL中包含了http协议,因此URLConnection对象接收到服务端反馈的应答消息头以后,就可以按照http协议将自动对其进行拆包动作,所以最终呈现在浏览器中的内容中将不再显示这一部分信息。

        总结以上内容,将URL与URLConnection对象结合起来,能够帮助我们自动完成以下四个操作:

(1)  URL解析;

(2)  自动建立客户端Socket服务;

(3)  与指定服务器建立连接;

(4)  向服务端发送请求消息头。

        由此可知,URL和URLConnection在实现网络通讯的原理方面,并没有特别之处,仅仅帮助程序员自动完成了一些操作,降低了开发难度,提高了开发效率。

API文档:

        抽象类URLConnection是所有类的超类,它代表应用程序和URL之间的通信链接。此类的实例可用于读取和写入此URL引用的资源。

方法:

        public InputStream getInputStream() throws IOException:返回从此打开的连接读取的输入流。

        public OutputStream getOutputStream() throws IOException:返回写入到此连接的输出流。

以上两个方法正是体现了通过Socket实现客户端与服务端数据传输的功能。

        此外,注意到该类并未对外提供close方法,但URLConnection类的API文档末尾提到:完成请求后,在一个URLConnection的InputStream或OutputStream上调用close()方法可以释放与此实例相关的网络资源,除非特定的协议规范为其制定了其他行为。

(2)  URLConnection应用演示

        由于通过该类实例对象与服务端实现通讯的方式,也是使用Socket读取/写入流,因此不再对这一步骤进行详细说明。

代码20:

import java.io.*;import java.net.*; class URLConnectionDemo{public static void main(String[] args) throws IOException{//将用户输入URL封装为URL对象URL url = newURL("http://169.254.100.231:8080/mywebs/demo.html"); //建立与指定服务器的连接,获取URLConnection对象URLConnection conn = url.openConnection();             //获取Socket读取流,直接接收服务端发送的数据InputStream receive = conn.getInputStream();             byte[] buf = new byte[1024];int len = 0;while((len = receive.read(buf)) !=-1){System.out.println(newString(buf, 0, len, "UTF-8"));}}}
开启Tomcat服务器后,执行以上代码后,显示在命令行中的内容为:

<html>

        <body>

                <h1>主页</h1>

                <fonr size=5color=green>Welcome!</font>

                <div>

                        内容</br>

                        内容</br>

                        内容</br>

                </div>

        </body>

</html>

结果中就不再显示应答消息头了。正如上文所说,URLConnection接收来自服务端的数据后,从传输层向上传输至应用层的时候,由于该对象内部封装有http协议,因此能够将服务端发送的应答消息头去掉,也就是对其进行解析。而我们在前面的演示代码中,由于没有http协议,因此自定义浏览器虽然能够接收数据,但无法解析应答消息头,所以显示内容中除了网页数据以外,还包括了传输层标识头。

        下面我们通过使用URLConnection对象来对代码18进行修改,主要修改部分为work、analysis以及connection三个方法。work方法的作用是调用其余两个方法实现功能,其中analysis方法的作用是URL解析,因此直接删除即可。connection方法的作用通过建立客户端Socket服务,与目标服务端进行连接,发送请求消息头,最终接受服务端反馈的请求数据,由于URLConnection对象能够帮助我们完成前两步,因此只需修改这两部分即可。修改后的代码为,

代码21:

private void work(){//从文本框中获取用户输入的域名字符串String strUlr = tf.getText();             try{URL url = new URL(strUlr); URLConnection conn = url.openConnection();             InputStream receive = conn.getInputStream();                    //接收服务端发送的响应信息BufferedReader bufr =new BufferedReader(newInputStreamReader(receive, "UTF-8")); String msg = null;while((msg = bufr.readLine()) !=null){ta.append(msg);ta.append("\r\n");} }catch(IOException e){throw new RuntimeException("数据传输失败!");}}
以下为执行以上代码的效果,


显然,不再显示服务端应答消息头了。只不过我们的自定义浏览器中并没有html代码解析引擎,因此直接将文本内容和html代码显示在了文本区域中。


0 0
原创粉丝点击