Java 网络编程---分布式文件协同编辑器设计与实现
目录:
第一部分:Java网络编程知识
(一)简单的Http请求
一般浏览网页时,使用的时Ip地址,而IP(Internet Protocol,互联网协议)目前主要是IPv4和IPv6.
IP地址是一个32位整数,一般分成4个八位二进制,为了方便记忆一般将八位整数换算为一个0-255的十进制整数。
利用Http的这些知识就可以实现一个多线程下载器,以及爬虫的基础向web站点发送GET/POST请求:
(1)一个简单的多线程下载器
1 import java.net.*; 2 import java.io.RandomAccessFile; 3 import java.io.InputStream; 4 public class DownUtil 5 { 6 //下载路径 7 private String path; 8 //指定下载文件存储的位置 9 private String targetFile; 10 //定义使用多少线程下载 11 private int threadNum; 12 //定义下载线程对象 13 private DownThread[] threads; 14 //定义下载文件的总大小 15 private int fileSize; 16 17 public DownUtil(String path,String target,int threadNum) 18 { 19 this.path = path; 20 this.targetFile=target; 21 this.threadNum=threadNum; 22 //初始化threads数组 23 threads=new DownThread[threadNum]; 24 } 25 public void download() throws Exception 26 { 27 URL url = new URL(path); 28 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 29 conn.setConnectTimeout(5*1000); 30 conn.setRequestMethod("GET"); 31 conn.setRequestProperty( 32 "Accept", 33 "image/gift,image/jpeg,image/pjpeg,image/pjpeg" 34 +"application/x-shockwave-flash,application/xaml+xml" 35 +"application/vnd.ms-xpsdocument,application/x-ms-xbap" 36 +"application/x-ms-application,application/vnd.ms-excel" 37 +"application/vnd.ms-powerpoint,application/msword,*/*" 38 ); 39 conn.setRequestProperty("Accept-Language","zh-CN"); 40 conn.setRequestProperty("Charset","UTF-8"); 41 conn.setRequestProperty("Connection","Keep-Alive"); 42 //获得文件大小 43 fileSize = conn.getContentLength(); 44 conn.disconnect(); 45 int currentPartSize=fileSize/threadNum+1; 46 RandomAccessFile file = new RandomAccessFile(targetFile,"rw"); 47 //设置本地文件大小 48 file.setLength(fileSize); 49 file.close(); 50 for(int i=0;i<threadNum;i++) 51 { 52 //计算每个线程开始的位置 53 int startPos = i*currentPartSize; 54 //每个线程使用一个RandomAccessFile下载 55 RandomAccessFile currentPart=new RandomAccessFile(targetFile,"rw"); 56 //定位线程下载的位置 57 currentPart.seek(startPos); 58 //创建下载线程 59 threads[i]=new DownThread(startPos,currentPartSize,currentPart); 60 //启动线程 61 threads[i].start(); 62 } 63 } 64 //获取下载的完成比 65 public double getCompleteRate() 66 { 67 //统计多个线程已经下载的总大小 68 int sumSize=0; 69 for(int i=0;i<threadNum;i++) 70 { 71 sumSize+=threads[i].length; 72 } 73 return sumSize*1.0/fileSize; 74 } 75 private class DownThread extends Thread 76 { 77 //当前的下载位置 78 private int startPos; 79 //当前线程负责下载的文件的大小 80 private int currentPartSize; 81 //当前线程需要下载文件块 82 private RandomAccessFile currentPart; 83 //定义该现场当前已经下载的字节数 84 public int length; 85 public DownThread(int startPos,int currentPartSize,RandomAccessFile currentPart) 86 { 87 this.startPos=startPos; 88 this.currentPartSize=currentPartSize; 89 this.currentPart=currentPart; 90 } 91 public void run() 92 { 93 try 94 { 95 URL url = new URL(path); 96 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 97 conn.setConnectTimeout(5*1000); 98 conn.setRequestMethod("GET"); 99 conn.setRequestProperty( 100 "Accept", 101 "image/gift,image/jpeg,image/pjpeg,image/pjpeg" 102 +"application/x-shockwave-flash,application/xaml+xml" 103 +"application/vnd.ms-xpsdocument,application/x-ms-xbap" 104 +"application/x-ms-application,application/vnd.ms-excel" 105 +"application/vnd.ms-powerpoint,application/msword,*/*" 106 ); 107 conn.setRequestProperty("Accept-Language","zh-CN"); 108 conn.setRequestProperty("Charset","UTF-8"); 109 InputStream inStream=conn.getInputStream(); 110 //跳过startPos个字节,只下载自己负责的那部分文件 111 inStream.skip(this.startPos); 112 byte[] buffer=new byte[1024]; 113 int hasRead=0; 114 //读取网络数据,并写入文件 115 while(length<currentPartSize && (hasRead=inStream.read(buffer))!=-1) 116 { 117 currentPart.write(buffer,0,hasRead); 118 length+=hasRead; 119 } 120 currentPart.close(); 121 inStream.close(); 122 } 123 catch (Exception e) 124 { 125 e.printStackTrace(); 126 } 127 } 128 } 129 }
(2)发生GET/POST请求
1 import java.net.URLConnection; 2 import java.net.URL; 3 import java.util.Map; 4 import java.util.List; 5 import java.io.BufferedReader; 6 import java.io.InputStreamReader; 7 import java.io.PrintWriter; 8 class GetPostTest 9 { 10 /** 11 *想指定URL发送GET请求 12 *@param url 发送请求的URL 13 *@param param 请求参数,格式满足key=value&key2=value2的形式 14 *@return URL 代表远程资源的响应 15 */ 16 public static String sendGet(String url,String param) 17 { 18 String result = ""; 19 String urlName=url+"?"+param; 20 try 21 { 22 URL realUrl=new URL(urlName); 23 //打开和URL之间的连接 24 URLConnection conn=realUrl.openConnection(); 25 //设置通用的请求属性 26 conn.setRequestProperty("accept","*/*"); 27 conn.setRequestProperty("connection","Keep-Alive"); 28 conn.setRequestProperty("user-agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2906.0 Safari/537.36"); 29 //建立实际链接 30 conn.connect(); 31 Map<String,List<String>> map =conn.getHeaderFields(); 32 //遍历所有相应头字段 33 for(String key:map.keySet()) 34 { 35 System.out.println(key+"---->"+map.get(key)); 36 } 37 try( 38 //定义BufferedReader输入流来读取URL响应 39 BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(),"utf-8"))) 40 { 41 String line; 42 while((line=in.readLine())!=null) 43 { 44 result+="\n"+line; 45 } 46 } 47 } 48 catch (Exception e) 49 { 50 System.out.println("发送GET请求出现异常!"+e); 51 e.printStackTrace(); 52 } 53 return result; 54 } 55 /** 56 *想指定URL发送POST请求 57 *@param url 发送请求的URL 58 *@param param 请求参数,格式满足key=value&key2=value2的形式 59 *@return URL 代表远程资源的响应 60 */ 61 public static String sendPost(String url,String param) 62 { 63 String result=""; 64 try 65 { 66 URL realUrl=new URL(url); 67 //打开和URL之间的连接 68 URLConnection conn=realUrl.openConnection(); 69 //设置通用的请求属性 70 conn.setRequestProperty("accept","*/*"); 71 conn.setRequestProperty("connection","Keep-Alive"); 72 conn.setRequestProperty("user-agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2906.0 Safari/537.36"); 73 conn.setDoOutput(true); 74 conn.setDoInput(true); 75 try( 76 //获取URLConnection对象对应的输出流 77 PrintWriter out =new PrintWriter(conn.getOutputStream()) ) 78 { 79 //发送请求参数 80 out.print(param); 81 //flush输出流的缓冲 82 out.flush(); 83 } 84 try( 85 //定义BufferedReader输入流来读取URL响应 86 BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(),"utf-8"))) 87 { 88 String line; 89 while((line=in.readLine())!=null) 90 { 91 result+="\n"+line; 92 } 93 } 94 } 95 catch (Exception e) 96 { 97 System.out.println("发送POST请求出现异常!"+e); 98 e.printStackTrace(); 99 } 100 return result; 101 } 102 public static void main(String[] args) 103 { 104 //String s =GetPostTest.sendGet("https://www.jd.com",null); 105 //System.out.println(s); 106 String s1=GetPostTest.sendPost("http://my.just.edu.cn/","user=1245532105&pwd=095493"); 107 String s2 =GetPostTest.sendGet("http://my.just.edu.cn/index.portal",null); 108 System.out.println(s1); 109 } 110 }
(二)基于TCP协议的网络编程(使用Socket进行通信)
TCP协议一般是和IP协议一起使用的,TCP/IP协议属于一种可靠的网络协议,它可以在通信的两端各建立一个Socket,使得两端形成一个虚拟的网络链路。于是两端的程序就可以通过这个虚拟链路进行通信。
通过安装IP协议,可以保证计算机之间发送和接受数据,但是无法解决数据分组在传输过程中的问题。所以需要TCP协议来提供可靠并且无差错的通信服务。
通过java的封装好的Socket可以实现一个简单的聊天程序。这个程序由服务端程序和客户端程序组成,为了实现用户聊天需要在服务端除了Server用于接受客户端请求并建立对应的Socket,还得通过一个ServerThread来为每个客户端建立一个线程用于监听客户端的消息,并向客户端发送消息。而在客户端除了一个Client程序用于建立和服务器端的Socket并且将用户输入的数据发送给服务器端,还需要一个ClientThread用来建立一个线程用于监听服务器端发来的数据并显示出来。
1 import java.io.PrintStream; 2 import java.io.IOException; 3 import java.net.ServerSocket; 4 import java.net.Socket; 5 import java.util.List; 6 import java.util.Collections; 7 import java.util.ArrayList; 8 import java.lang.Thread; 9 class MyServer 10 { 11 public static final int SERVER_PORT=30000; 12 //利用MyMap保存每个客户端名字和对应的输出流 13 public static MyMap<String,PrintStream> clients = new MyMap<String,PrintStream>(); 14 public void init() 15 { 16 try 17 ( 18 //建立监听的ServerSocket 19 ServerSocket ss = new ServerSocket(SERVER_PORT)) 20 { 21 //采用死循环一直接受客户端的请求 22 while(true) 23 { 24 Socket s= ss.accept(); 25 new MyServerThread(s).start(); 26 } 27 } 28 catch (IOException ex) 29 { 30 System.out.println("服务器启动失败,请检查端口:"+SERVER_PORT+"是否已经被占用?"); 31 } 32 } 33 public static void main(String[] args) 34 { 35 MyServer server=new MyServer(); 36 server.init(); 37 } 38 }
1 import java.net.Socket; 2 import java.io.BufferedReader; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.io.PrintStream; 6 public class MyServerThread extends Thread 7 { 8 //定义当前线程所处理的Socket 9 private Socket s=null; 10 //该Socket对应的输入流 11 BufferedReader br=null; 12 //该Socket对应的输出流 13 PrintStream ps=null; 14 public MyServerThread(Socket s) throws IOException 15 { 16 this.s=s; 17 } 18 public void run() 19 { 20 try 21 { 22 //获取该Socket对应得输入流 23 br=new BufferedReader(new InputStreamReader(s.getInputStream())); 24 //获取该Socket对应的输出流 25 ps=new PrintStream(s.getOutputStream()); 26 String line = null; 27 //采用循环不断地从Socket中读取客户端发来的数据 28 while( (line=br.readLine())!=null) 29 { 30 //如果读取到MyProtrocol.USER_ROUND开始,并以其结束则可以确定读到的是用户的登录名 31 if(line.startsWith(MyProtocol.USER_ROUND)&&line.endsWith(MyProtocol.USER_ROUND)) 32 { 33 //得到真实消息 34 String userName=getRealMsg(line); 35 //如果用户名重复 36 if(MyServer.clients.map.containsKey(userName)) 37 { 38 System.out.println("用户名重复"); 39 ps.println(MyProtocol.NAME_REP); 40 } 41 else 42 { 43 System.out.println(userName+"登陆成功"); 44 ps.println(MyProtocol.LOGIN_SUCCESS); 45 MyServer.clients.put(userName,ps); 46 } 47 } 48 //如果读到以MyProtocol.PRIVATE_ROUND开始,并以其结束,则可以确定是私聊信息,私聊信息只向制定输出流发送 49 else if(line.startsWith(MyProtocol.PRIVATE_ROUND)&&line.endsWith(MyProtocol.PRIVATE_ROUND)) 50 { 51 //得到真实消息 52 String userAndMsg = getRealMsg(line); 53 //以SPLIT_SIGN分割,前半部分是私聊用户名,后一部分是内容 54 //System.out.println(userAndMsg); 55 //System.out.println(MyProtocol.SPLIT_SIGN); 56 String user = userAndMsg.split(MyProtocol.SPLIT_SIGN)[0]; 57 String msg = userAndMsg.split(MyProtocol.SPLIT_SIGN)[1]; 58 //获取私聊用户对应的输出流,并发送私聊信息 59 MyServer.clients.map.get(user).println(MyServer.clients.getKeyByValue(ps)+"悄悄对你说:"+msg); 60 } 61 //公聊,对所有Socket发 62 else 63 { 64 //获取真实消息 65 String msg=getRealMsg(line); 66 //遍历clients中的每个输出流 67 for(PrintStream clientPs:MyServer.clients.valueSet()) 68 { 69 clientPs.println(MyServer.clients.getKeyByValue(ps)+"说:"+msg); 70 } 71 } 72 } 73 } 74 //捕获到异常,表明该Socket有问题,将该程序对应的输出流从Map中删除 75 catch (IOException ex) 76 { 77 MyServer.clients.removeByValue(ps); 78 System.out.println(MyServer.clients.map.size()); 79 //关闭网络和IO资源 80 try 81 { 82 if(br!=null) br.close(); 83 if(ps!=null) ps.close(); 84 if(s!=null) s.close(); 85 } 86 catch (IOException e) 87 { 88 e.printStackTrace(); 89 } 90 } 91 } 92 //将读到的内容去掉前后的协议,恢复成真实数据 93 private String getRealMsg(String line) 94 { 95 return line.substring(MyProtocol.PROTOCOL_LEN,line.length()-MyProtocol.PROTOCOL_LEN); 96 } 97 }
1 import java.io.PrintStream; 2 import java.io.IOException; 3 import java.net.UnknownHostException; 4 import java.io.InputStreamReader; 5 import java.io.BufferedReader; 6 import java.net.ServerSocket; 7 import java.net.Socket; 8 import java.net.InetAddress; 9 import javax.swing.JOptionPane; 10 class MyClient 11 { 12 private static final int SERVER_PORT=30000; 13 private Socket socket; 14 private PrintStream ps; 15 private BufferedReader brServer; 16 private BufferedReader keyIn; 17 public void init() 18 { 19 try 20 { 21 //初始化代表键盘的输入流 22 keyIn = new BufferedReader(new InputStreamReader(System.in)); 23 //链接到服务器 24 socket = new Socket("127.0.0.1",SERVER_PORT); 25 //获取该Socket对应的输入流和输出流 26 ps = new PrintStream(socket.getOutputStream()); 27 brServer=new BufferedReader(new InputStreamReader(socket.getInputStream())); 28 String tip=""; 29 //采用循环不断弹出对话框要求输入用户名 30 while(true) 31 { 32 String userName=JOptionPane.showInputDialog(tip+"输入用户名"); 33 //用户输入用户名后前后加上协议字符串后发送 34 ps.println(MyProtocol.USER_ROUND+userName+MyProtocol.USER_ROUND); 35 //读取服务器的响应 36 String result = brServer.readLine(); 37 //如果用户名重复则开始下次循环 38 if(result.equals(MyProtocol.NAME_REP)) 39 { 40 tip="用户名重复,请重试!"; 41 continue; 42 } 43 //如果服务器返回登陆成功,则循环结束 44 if(result.equals(MyProtocol.LOGIN_SUCCESS)) 45 { 46 break; 47 } 48 } 49 } 50 //补货到异常,关闭网络资源,并推出程序 51 catch (UnknownHostException ex) 52 { 53 System.out.println("找不到远程服务器,请确定服务器已经启动!"); 54 closeRs(); 55 System.exit(1); 56 } 57 catch(IOException ex) 58 { 59 System.out.println("网络异常!请重新登陆"); 60 closeRs(); 61 System.exit(1); 62 } 63 //以该socket对应的输入流启动Client线程 64 new MyClientThread(brServer).start(); 65 } 66 //定义一个读取键盘输出,并向网络发送的方法 67 private void readAndSend() 68 { 69 try 70 { 71 //不断地读取键盘的输入 72 String line=null; 73 while((line=keyIn.readLine())!=null) 74 { 75 //如果发送的信息中有冒号,并且以//开头,则认为发送私聊信息 76 if(line.indexOf(":")>0&&line.startsWith("//")) 77 { 78 line=line.substring(2); 79 //System.out.println(MyProtocol.PRIVATE_ROUND+line.split(":")[0]+MyProtocol.SPLIT_SIGN+line.split(":")[1]+MyProtocol.PRIVATE_ROUND); 80 ps.println(MyProtocol.PRIVATE_ROUND+line.split(":")[0]+MyProtocol.SPLIT_SIGN+line.split(":")[1]+MyProtocol.PRIVATE_ROUND); 81 } 82 else 83 { 84 ps.println(MyProtocol.MSG_ROUND+line+MyProtocol.MSG_ROUND); 85 } 86 } 87 } 88 //捕获到异常,关闭网络资源,并退出程序 89 catch (IOException ex) 90 { 91 System.out.println("网络异常!请重新登陆"); 92 closeRs(); 93 System.exit(1); 94 } 95 } 96 //关闭Socket、输入流、输出流的方法 97 private void closeRs() 98 { 99 try 100 { 101 if(keyIn!=null) ps.close(); 102 if(brServer!=null) brServer.close(); 103 if(ps!=null) ps.close(); 104 if(socket!=null) keyIn.close(); 105 } 106 catch (IOException ex) 107 { 108 ex.printStackTrace(); 109 } 110 } 111 public static void main(String[] args) 112 { 113 MyClient client=new MyClient(); 114 client.init(); 115 client.readAndSend(); 116 } 117 }
1 import java.io.PrintStream; 2 import java.io.IOException; 3 import java.io.InputStreamReader; 4 import java.io.BufferedReader; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 import java.net.InetAddress; 8 import java.lang.Thread; 9 class MyClientThread extends Thread 10 { 11 //该客户端线程负责处理输入流 12 BufferedReader br=null; 13 public MyClientThread(BufferedReader br) 14 { 15 this.br=br; 16 } 17 public void run() 18 { 19 try 20 { 21 String line=null; 22 //不断的读取Socket输入流的内容,并将其打印输出 23 while((line=br.readLine())!=null) 24 { 25 System.out.println(line); 26 } 27 } 28 catch (Exception e) 29 { 30 e.printStackTrace(); 31 } 32 finally 33 { 34 try 35 { 36 if(br!=null) br.close(); 37 } 38 catch (IOException ex) 39 { 40 ex.printStackTrace(); 41 } 42 } 43 } 44 }
JDK1.4开始,Java提供了NIO API用于开发高性能的网络服务器,借助NIO可以不必为每个客户端都建立一个线程。下面我将利用NIO改变这个聊天程序。
第二部分:分布式文件协同编辑器
第一部分 Java网络编程知识
附件:《实验指导报告书》
分布式文件协同编辑器
实验指导书--1
一、实验目的
加深对分布式系统基本概念的理解,灵活运用多种分布式互斥与同步访问的算法;掌握网络编程的基本方法,熟悉Socket套接字的使用,实现网络间的通信程序;设计并初步实现一个“分布式文件协同编辑器”原型系统。
二、实验要求
1、有N个网络用户编辑同一磁盘上的多个文件,文件的存取服务由文件服务器完成,网络上的用户通过客户端软件完成协同编辑工作。编辑器架构如图1所示:
2、设计并实现分布式互斥与同步访问,实现多用户协同编辑。
3、设计并初步实现一个“分布式文件协同编辑器”。
4、可以在Window或Linux下完成。
三、实验内容
实验内容由两部分组成:
第一部分:编写文件服务器程序,详细要求有:
- 实现对磁盘文件的存取服务。
- 实现与客户端软件的文件传输服务(客户端指定文件名称,通过Socket实现)
- 不能使用集中控制策略。
第二部分:编写客户端软件,具体要求如下:
- 实现对文件简单的编辑/浏览功能(只要能查看/改变文件内容即可);
- 实现与文件服务器的文件传输功能(客户端指定文件名称,通过Socket实现);
- 实现多个客户端软件之间的通讯功能,能实现协同工作的功能;
- 实现分布式的互斥编辑功能。
四、实验方法
1、实验有两个方案(同学们也可自己设计新的方案):
方案一:获取并发用户列表的方法:客户端软件在访问文件时,先在子网内广播信息,访问该文件的其它客户端软件应答。
方案二:分布式互斥访问可以使用令牌环算法。
2、实现分布式互斥编辑功能(同学们也可自己设计新的方案):
1) 多个客户可以同时浏览文件内容(已经提交的版本)。
2) 当文件加互斥锁时,多个客户也可以同时浏览文件内容(旧版本),但是只能由加互斥锁的用户编辑文件(未提交的版本),而且提交之后,必须通知其他浏览该文件的用户,以便其它用户获得最新版本的内容。
3) 进入编辑状态之前,首先要获得互斥锁。而且在任意时刻,只能一个用户对文件进行编辑。