今天我要向大家介绍的是自己编写的一个比较简单的服务器和客户机程序,注意一下哦,比较简单。好了,闲话休提,砸门直入主题。
小编先从客户机和服务器的模型开始讲解。简单来说,我们实现的这种模型呢,我们每一个用户称为一个客户机,用户之间的通信之间需要一个中转,所有客户机的通信都依托于这个中转,很明显,这个中转,就是砸门的服务器了。整个模型比较简单明了,那么,接下来我们就直接进入实现阶段。
我们从实现服务器开始。Java提供了这样的一个类,ServerSocket。我们通过实例化它的方式来创建一个服务器。注意一下,创建服务器需要一个参数port。port指的是端口号。我们知道一台计算机上运行着许多程序,那么,对于另一台计算机来说,它想要连接到这台服务器的这个程序,该如何区分呢?首先当然是根据IP连接到这台计算机,那么,然后呢?对的,你没有想错,就是通过我们的port端口号了。正常情况下,计算机会自动我们创建的程序分配一个port,但是,因为我们创建的是提供给别的计算机连接的服务器,所以我们需要指定一个端口号供给外部计算机连接。注意一下,计算机有的端口号已经被其他程序占用了,要想查看,你可以通过在cmd命令行输入netstat -an来查询。在这里,我们给定我们的端口号为空闲的9999。
1 public static void main(String[] args) { 2 new Server().CreateServer(9999); 3 } 4 public void CreateServer(int port){ 5 try { 6 ServerSocket serversocket = new ServerSocket(port); 7 } catch (IOException e) { 8 e.printStackTrace(); 9 } 10 }
接下来,我们让服务器进入等待客户机连接的状态。通过查看ServerSocket源码我们会发现,它里面有一个accept方法,其返回值是Socket类型。
1 Socket socket = serversocket.accept();
Socket是Java中的另一个用于通信的类, 我们称它为套接字。事实上,小编是这样看的,Socket相当于建立在服务器和客户端之间的一根通道。我们可以从socket获取它的输入输出流对象,也就是我们的InputStream和OutputStream。这两个输入输出都是采用字节传输的方法,如果你想要读取一行数据可以通过BufferReader bufferreader = new BufferedReader(new InputStreamReader(is))的方式,将InputStream转化成bufferRead之后调用readLine方法就可以了。需要注意的是对于readLine这个方法,/r/n表示消息的读取结束。因此需要对消息进行+“/r/n”的操作来保证读取的正常进行。
因为在等待客户机连接的时候,程序进入阻塞状态,这个时候程序无法进行下去,而且,这样的写法也只能够供给一个客户机连接。为了能够实现简单客户机和服务器的对话,我们需要采用多线程的方式去实现它,并且将实例化socket对象以及之后的这部分代码放入到while循环当中。为了能够实现客户机与客户机的通信,我们建立了一个消息处理类,并且实例化了一个数组队列去存储它,然后将这个队列作为消息处理类的一个属性。这样所有的问题就得到了完美的解决。
1 public void creatserver(int port) { 2 try { 3 ServerSocket serversocket = new ServerSocket(port); 4 System.out.println("服务器已经开启,正在等待客户机连接..."); 5 while (true) { 6 Socket sk = serversocket.accept(); 7 chatMessage cm = new chatMessage(sk, list); 8 list.add(cm); 9 cm.start(); 10 } 11 } catch (IOException e) { 12 e.printStackTrace(); 13 } 14 }
消息处理类当中的内容,主要是实现对消息的封装处理等等,以及对消息的鉴别。诸如消息是群发还是私发,如果是群发,那么遍历整个消息处理类的队列,如果是私发,那么同样需要遍历整个队列去寻找我们需要私发的用户名,然后将消息传给它。一些其他的注意就不再赘述了,小编在代码中也给出了相应的注释,特别提出一点,当socket关闭,连接断开,我们同样需要关闭我们的IO流,否则占用资源是比较严重的。
1 public class chatMessage extends Thread { 2 // 客户机的账号 3 public String username; 4 // 客户机的密码 5 public String password; 6 // // 客户机的开启状态 7 // public volatile boolean state; 8 // 客户机对象 9 public Socket s; 10 // 客户机的输入流 11 public InputStream is; 12 // 客户机的输出流 13 public OutputStream os; 14 // 缓冲(读一行数据) 15 public BufferedReader br; 16 // 存储所有客户机对象的数组队列 17 public ArrayList<chatMessage> list; 18 19 /** 20 * chatMessage的构造方法,包括IO流的建立 21 * 22 * @param s 23 * Socket对象 24 * @param list 25 * 存储chatMessage对象的数组队列 26 * @throws IOException 27 * 抛出IO异常 28 */ 29 public chatMessage(Socket s, ArrayList<chatMessage> list) 30 throws IOException { 31 this.s = s; 32 this.list = list; 33 is = s.getInputStream(); 34 os = s.getOutputStream(); 35 br = new BufferedReader(new InputStreamReader(is)); 36 } 37 38 /** 39 * 服务器转发消息 40 * 41 * @param str 42 * 转发的消息内容 43 * @throws IOException 44 * 抛出IO异常 45 */ 46 public void sendMessage(String str) throws IOException { 47 os.write((str + "\r\n").getBytes()); 48 } 49 50 /** 51 * 服务器把消息广播到所有的客户机 52 * 53 * @param str 54 * 广播的消息内容 55 * @param code 56 * 消息类型的判断,1表示为提示成员登录,2表示为广播消息 57 * @throws IOException 58 * 抛出的IO异常 59 */ 60 public void sendAllMessage(String str, int code) throws IOException { 61 for (int index = 0; index < list.size(); index++) { 62 if (list.get(index) != this && code == 1) { 63 list.get(index).sendMessage(str); 64 } else if (code == 2) { 65 list.get(index).sendMessage(str); 66 } 67 } 68 System.out.print(str + "\r\n"); 69 } 70 71 /** 72 * 读取来自客户端的消息 73 * 74 * @return 返回一个字符串 75 * @throws IOException 76 * 抛出IO异常 77 */ 78 public String readMessage() throws IOException { 79 return br.readLine(); 80 } 81 82 /** 83 * 服务器把消息转发给某人 84 * 85 * @param str1 86 * 消息内容 87 * @param str2 88 * 消息的接收人 89 */ 90 public void sendMessageToOthers(String str1, String str2) { 91 for (int i = 0; i < list.size(); i++) { 92 if (str2.equals(list.get(i).username)) { 93 try { 94 list.get(i).sendMessage(str1); 95 } catch (IOException e) { 96 e.printStackTrace(); 97 } 98 } 99 } 100 } 101 102 /** 103 * 线程启动 从客户机获取登录信息,分别赋给username以及password 给对应客户机发送“欢迎来到聊天室” 104 * 给所有的客户机发送username+“来到聊天室” 105 * 106 * 107 * 108 * 109 * 110 * 111 */ 112 public void run() { 113 try { 114 String str = readMessage(); 115 makeMessage(str); 116 sendMessage("欢迎来到聊天室"); 117 String string = username + "来到聊天室"; 118 sendAllMessage(str, 1); 119 String s; 120 while (!(s = readMessage()).equals("bye")) { 121 if (s.charAt(0) == '@') { 122 int nig = s.indexOf(" "); 123 String ns = s.substring(1, nig); 124 String valuestring = s.substring(nig + 1, s.length()); 125 for (int j = 0; j < list.size(); j++) { 126 if (ns.equals(list.get(j).username)) { 127 list.get(j).sendMessage( 128 username + "对你说:" + valuestring); 129 } 130 } 131 } else { 132 s = username + "说:" + s; 133 sendAllMessage(s, 2); 134 } 135 } 136 sendAllMessage(username + "离开了聊天室", 1); 137 close(); 138 } catch (IOException e) { 139 e.printStackTrace(); 140 } 141 } 142 143 /** 144 * 关闭所有IO 145 * 146 * @throws IOException 147 * 抛出IO异常 148 */ 149 public void close() throws IOException { 150 is.close(); 151 os.close(); 152 br.close(); 153 s.close(); 154 } 155 156 /** 157 * 对首次登录时客户机发送的登录信息进行处理,给username以及password赋值 158 * 159 * @param str 160 * 首次登录客户机发送的信息 161 */ 162 public void makeMessage(String str) { 163 int index = str.indexOf(","); 164 username = str.substring(0, index); 165 password = str.substring(index + 1, str.length()); 166 } 167 }
客户端的编写跟服务器差不多,主要需要注意的一些内容上的匹配,比如小编的服务器首先需要进行一次对账号密码内容的接收,也就是说需要读取一条消息后正常进行。小编稍微加了一点界面内容,比较简单,所以也比较一般,大家稍微看看,不必过多在意界面的内容。
这个是登录界面的代码:
1 package Client0802; 2 3 import java.awt.Dimension; 4 import java.awt.FlowLayout; 5 import java.awt.Font; 6 7 import javax.swing.JButton; 8 import javax.swing.JFrame; 9 import javax.swing.JLabel; 10 import javax.swing.JPasswordField; 11 import javax.swing.JTextField; 12 13 public class Login extends JFrame { 14 public static void main(String[] args) { 15 new Login().initUI(); 16 } 17 18 public void initUI() { 19 this.setTitle("Login"); 20 this.setSize(350, 300); 21 this.setLayout(new FlowLayout()); 22 this.setLocationRelativeTo(null); 23 this.setDefaultCloseOperation(3); 24 Dimension dim1 = new Dimension(100, 60); 25 Dimension dim2 = new Dimension(200, 60); 26 Font font = new Font("宋体", Font.BOLD, 20); 27 JLabel jlone = new JLabel("账号 :"); 28 jlone.setPreferredSize(dim1); 29 jlone.setFont(font); 30 this.add(jlone); 31 JTextField jtf = new JTextField(); 32 jtf.setPreferredSize(dim2); 33 jtf.setFont(font); 34 this.add(jtf); 35 JLabel jltwo = new JLabel("密码 :"); 36 jltwo.setPreferredSize(dim1); 37 jltwo.setFont(font); 38 this.add(jltwo); 39 JPasswordField jpwf = new JPasswordField(); 40 jpwf.setPreferredSize(dim2); 41 jpwf.setFont(font); 42 this.add(jpwf); 43 JButton jbt = new JButton("登录"); 44 jbt.setPreferredSize(new Dimension(80, 40)); 45 this.add(jbt); 46 47 this.setVisible(true); 48 listener lis = new listener(this, jtf, jpwf); 49 jbt.addActionListener(lis); 50 } 51 52 public static void Log(String str) { 53 System.out.println(str); 54 } 55 }
这个是处理点击登录的监听器:
1 package Client0802; 2 3 import java.awt.Dimension; 4 import java.awt.FlowLayout; 5 import java.awt.TextArea; 6 import java.awt.event.ActionEvent; 7 import java.awt.event.ActionListener; 8 import java.io.BufferedReader; 9 import java.io.IOException; 10 import java.io.InputStream; 11 import java.io.InputStreamReader; 12 import java.io.OutputStream; 13 import java.net.Socket; 14 import java.net.UnknownHostException; 15 16 import javax.swing.JButton; 17 import javax.swing.JFrame; 18 import javax.swing.JPasswordField; 19 import javax.swing.JTextField; 20 21 public class listener implements ActionListener { 22 public JFrame jf; 23 public JFrame newjf; 24 public InputStream is; 25 public OutputStream os; 26 public BufferedReader br; 27 public JTextField jtf, cou; 28 public JPasswordField jpwf; 29 30 public listener(JFrame jf, JTextField jtf, JPasswordField jpwf) { 31 this.jf = jf; 32 this.jtf = jtf; 33 this.jpwf = jpwf; 34 } 35 36 /* 37 * 实例化客户机,连接到服务器 实例化IO流 将得到的账户和密码发送给服务器 关闭原有登录界面 建立一个显示界面 38 */ 39 public void actionPerformed(ActionEvent e) { 40 try { 41 Socket s = new Socket("127.0.0.1", 7777); 42 System.out.print("连接到了服务器"); 43 is = s.getInputStream(); 44 os = s.getOutputStream(); 45 br = new BufferedReader(new InputStreamReader(is)); 46 String s1 = jtf.getText(); 47 String s2 = String.valueOf(jpwf.getPassword()); 48 sendMessage(s1 + "," + s2 + "\r\n"); 49 jf.dispose(); 50 newjf = new JFrame(); 51 newjf.setTitle("communication..."); 52 newjf.setSize(800, 600); 53 newjf.setLocationRelativeTo(null); 54 newjf.setDefaultCloseOperation(3); 55 newjf.setLayout(new FlowLayout()); 56 TextArea ta = new TextArea(); 57 ta.setPreferredSize(new Dimension(790, 300)); 58 newjf.add(ta); 59 cou = new JTextField(); 60 cou.setPreferredSize(new Dimension(700, 150)); 61 newjf.add(cou); 62 JButton jb = new JButton("发送"); 63 jb.setPreferredSize(new Dimension(80, 150)); 64 newjf.add(jb); 65 newjf.setVisible(true); 66 getMessage gm = new getMessage(br, ta); 67 gm.start(); 68 jb.addActionListener(new ActionListener() { 69 public void actionPerformed(ActionEvent e) { 70 try { 71 os.write((cou.getText() + "\r\n").getBytes()); 72 cou.setText(""); 73 } catch (IOException e1) { 74 e1.printStackTrace(); 75 } 76 } 77 }); 78 } catch (UnknownHostException e1) { 79 e1.printStackTrace(); 80 } catch (IOException e1) { 81 e1.printStackTrace(); 82 } 83 } 84 85 /** 86 * 发送消息到服务器 87 * 88 * @param str 89 */ 90 public void sendMessage(String str) { 91 try { 92 os.write(str.getBytes()); 93 } catch (IOException e) { 94 e.printStackTrace(); 95 } 96 } 97 98 /** 99 * 初始化一个显示界面 100 */ 101 public void initUI() { 102 newjf = new JFrame(); 103 newjf.setTitle("communication..."); 104 newjf.setSize(800, 600); 105 newjf.setLocationRelativeTo(null); 106 newjf.setDefaultCloseOperation(3); 107 newjf.setLayout(new FlowLayout()); 108 TextArea ta = new TextArea(); 109 ta.setPreferredSize(new Dimension(790, 300)); 110 newjf.add(ta); 111 cou = new JTextField(); 112 cou.setPreferredSize(new Dimension(700, 150)); 113 newjf.add(cou); 114 JButton jb = new JButton("发送"); 115 jb.setPreferredSize(new Dimension(80, 150)); 116 newjf.add(jb); 117 jb.addActionListener(new ActionListener() { 118 public void actionPerformed(ActionEvent e) { 119 try { 120 os.write((cou.getText() + "\r\n").getBytes()); 121 cou.setText(""); 122 } catch (IOException e1) { 123 e1.printStackTrace(); 124 } 125 } 126 }); 127 newjf.setVisible(true); 128 boolean temp = true; 129 while (temp) { 130 temp = false; 131 getMessage gm = new getMessage(br, ta); 132 gm.start(); 133 } 134 } 135 }
这个是用于控制接收服务器消息的线程类:
1 package Client0802; 2 3 import java.awt.TextArea; 4 import java.io.BufferedReader; 5 import java.io.IOException; 6 7 public class getMessage extends Thread { 8 public BufferedReader br; 9 public TextArea ta; 10 11 public getMessage(BufferedReader br, TextArea ta) { 12 this.br = br; 13 this.ta = ta; 14 } 15 16 public void run() { 17 while (true) { 18 try { 19 String str = br.readLine(); 20 if (str != null && str != "") { 21 ta.append(str + "\n"); 22 } 23 } catch (IOException e) { 24 e.printStackTrace(); 25 } 26 } 27 } 28 }
大概内容就这么多拉,其实小编还有很多内容没有设计到,比如说发送图片啊,比如说文件的传输啊,这些都还没有动手实现,但是,整个过程其实都差不太多,只是传输用到的IO流类名不太一样而已,这不是什么大问题。我相信以大家的聪明才智都能够解决!最后小编将就通信协议这一块稍微做一点总结。
协议是什么。协议是指双方共同设计的必须满足的要求。在通信这一块,在客户机服务器模型这一块,就是指规定消息的编写格式。这也就是为什么小编之前说的编写客户机和服务器的内容查差不了多少了。对于一个完整的通信系统来说,协议非常重要,就像老师群发了一封邮件给所有人,但是,邮件是用日语写的,大家看不懂,这就是导致的后果。协议也一样,客户机给服务器发送许许多多的字节,服务器无法区分有几部分,每一部分是什么意思,那么,服务器就无法对消息进行处理。就像一段暗号或者说是密文,你可以这么去定义你的协议,比如,消息的第几个字节表示什么,消息第几个字节到第几个字节代表什么。当然这还是比较简单的通信协议,各位看官可以去网上查一查那些比较有名的软件的协议,QQ、微信等等。当然,有一些软件的协议是不公开的。
东西就讲到这啦,小编最近在写一个安卓的客户端,估摸着也差不太多,欢迎下次再来!