一、创建一个简单的服务器:
1、 在指定端口创建一个java.net.ServerSocket服务器对象。
java.net.ServerSocket server = new java.net.ServerSocket(port);
2、 让服务器对象进入等待状态,等待客户机来连结。并且设定在while(true){}中循环等待,连结了一个客户端后,等待下一个客户机的连结。
java.net..Socket client = server.accept();
3、 从上一步的连结对象上面得到输入输出流。
4、 通过输入流从客户端读取数据,从输出流向客户端中写入数据。
5、 在命令行通过telnet连结上启动的服务器,可以互相发送字符串。
二、多线程的服务器
客户机往往有多个,对于一个服务器来说,处理多个客户机的方法就是每接入一个客户机就启动一个连结对象来处理与客户机的通信,这就可以通过添加一个用来处理服务器端与客户机的连结对象的线程类完成。每连入一个客户机就启动一个处理线程。线程类中放入读取客户机消息并发送消息的方法。
public class ClientThread extends Thread{
private java.net.Socket client;
private java.io.OutputStream ous;
public ClientThread(java.net.Socket client){
//创建这个线程对象时,传入一个他要处理的连结对象
this.client = client;
}
public void run(){
processClient(this.client);
}
/**
* 发送消息给客户端
* @param mag:从客户端读到的字符串
*/
public void senMsg2Me(String msg){
try{
byte[] data = msg.getBytes();
ous.write(data);//向输出对象中写入数据
ous.flush();//强制输出
}catch(Exception ef){
ef.printStackTrace();
}
}
/**
* 处理客户进入的连结对象
* @param client
*/
private void processClient(java.net.Socket client){
try{
//3.从socket连结对象上调用方法得到输入输出流
ous = client.getOutputStream();
InputStream ins = client.getInputStream();
//4.从输入流中读取数据,向输出流中写入数据。
String s = "欢迎来聊天!\r\n";
this.senMsg2Me(s);
//调用读取字符串的方法,从输入流中读取一个字符串
String inputS = this.readString(ins);
while(!inputS.equals("bye")){
System.out.println("客户机说:"+inputS);
s = "服务器收到的是:"+inputS+"\r\n";
this.senMsg2Me(s);
inputS = this.readString(ins);
}
s = "你好,欢迎再来。\r\n";
this.senMsg2Me(s);
client.close();
}catch(Exception ef){
ef.printStackTrace();
}
}
/**
* 从输入流对象中读取字节,拼成一个字符串返回
*
* @param ins:输入流对象
* @return
* @throws IOException
*/
private String readString(InputStream ins) throws Exception{
//创建一个字符串缓冲区
StringBuffer stb = new StringBuffer();
char c = 0;
while(c!=13||c!=-1){
int i = ins.read();
System.out.println("读到的字节是:"+i);
c = (char)i;
stb.append(c);
}
//将独到的字节数组转换为字符串,并去掉尾部空格
String inputS = stb.toString().trim();
return inputS;
}
}
其中包含一个读取字符串的方法readString(InputStream ins),是否还有更简单的解决方法?
三、读写字符串的方法
最初解决字符串读取的方法,就是创建一个字符串缓冲区,循环着一个一个字节的从客户机的输入流读取,并将字节转换为char类型,通过append方法组合成为字节数组。读到回车符时(即13),将append的字节数组转换为字符串输出到客户端。不过这个方法在读取中文字符串时会有乱码,因为一个中文需要两个字节来表示,而这种方法只能一个一个的来读取,转换成字符串时就会出错不能还原成为中文。
不过其实有个BufferedReader类,可以从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组、行的高校读取。
首先用BufferedReader包装输入流,缓冲指定文件的输入。
//将输入流封ins装为可以读取一行字符串,也就是以\r\n皆为的字符串
BufferedReader brd = new BufferedReader(new InputStreamReader(ins));
然后调用readLine()方法读取字节并将其转换为字符返回是更为高效的方法
//调用读取字符串的方法,从输入流中读取一个字符串
String inputS = brd.readLine();
四、公聊服务器的实现
公聊服务器中,每登录一个新的客户机,会通知其他用户有一个新用户上线,下线同理;另外,某一个客户机发送的消息,服务器会转发给其他所有在线用户,实现群聊的功能。流程也更为复杂:
1、 每有一个新的客户端连结,服务器会要求输入用户名及密码。
2、 服务器验证用户信息是否正确。若信息不相符,则提示用户信息有错。
3、 若通过验证,则给其他客户机发送消息:该用户上线,目前在线人数几人。
4、 在线用户可向服务器发送消息,服务器会转发给其他所有的在线用户。
5、 当某个客户端退出,服务器通知其他客户机:某用户下线,当前在线人数几人。
这里公聊的服务器增加了验证登陆信息、转发消息等多个功能,也就需要增加几个类来辅助。首先对于登陆用户来说,需要验证其用户名和密码,曾设一个DaoTools类:负责验证用户名和密码是否正确。验证用户信息的过程可以首先模拟一个数据库,用一个map存储用户名和对应的用户信息,在运行到验证模块前就将设置好的数据存入MAP,验证时,通过用户名得到对应的用户类,也就得到了该用户密码,验证是否符合。因此,也就需要创建一个用户信息类,其中存储用户名,密码以及以后可能用到的IP地址、登录时间等等信息。转发消息作为另外一个独立功能,也需要建立一个辅助类,其中放置通知上下线以及转发消息的方法。因为上线用户个数不定,可用一个队列来存放客户端处理线程类每增加一个客户连入,则启动一个处理线程,加入到队列中,而当前在线人数就是该队列的长度。转发消息则只要遍历队列,发送消息即可达成。
综上,总共需要设置五个类:
ChatServer类:创建服务器并等待连结。
ServerThread类:处理连结的线程类,读取客户端输入流并发送给客户端。
UserInfo类:用户信息类,存放用户属性。
DaoTools类:一个辅助类,使用时并不需要创建对象,直接调用其中存放的方法就好,因此其中的方法都是static方法。这个类中存放的是验证用户登录的方法。
ChatTools类:同样不需要创建对象,是一个辅助方法,转发客户机发送的消息,并有将新上线的用户即一个线程加入队列或者下线用户移除队列的方法。
上述两个辅助类都只只要调用其中方法的,可能觉得创建这两个类不是那么必要,不过因为类的职责要单一,这样也使得编程思路更加的清晰。
下面是这两个辅助类的代码:
/**
* 转发消息的辅助类,只需调用该类的方法,不用创建对象,所以皆为静态方法
* @author Administrator
*
*/
public class ChatTools {
private static ArrayList<ClientThread> ctList = new ArrayList<ClientThread>();
/**
* 添加连结线程到队列的方法
* @param client
*/
public static void addClient(ClientThread ct){
ctList.add(ct);
castMsg(ct.getOwnUser(),"我上线了,当前在线人数:"+ctList.size());
}
/**
*
* @param ct
*/
public static void removeClient(ClientThread ct){
ctList.remove(ct);
castMsg(ct.getOwnUser(),"我下线了,当前在线人数:"+ctList.size());
}
/**
* 转发消息的方法
* @param msg
*/
public static void castMsg(UserInfo sender,String msg){
msg = sender.getName()+":\r\n"+msg;
for(int i=0;i<ctList.size();i++){
ClientThread ct = ctList.get(i);
ct.senMsg2Me(msg);//发消息给每一个客户机
}
}
}
以及验证信息的辅助类:
//用户信息验证
public class DaoTools {
//用户信息数据库
private static java.util.Map<String, UserInfo> userDB = new java.util.HashMap<String, UserInfo>();
/**
* 登录验证的方法
* @param user
* @return
*/
public static boolean checkLogin(UserInfo user){
String name = user.getName();
String pwd = user.getPwd();
if(userDB.containsKey(name)){//如果用户名存在
//得到数据库中的用户和密码
UserInfo userInDB = userDB.get(name);
String pwdInDB = userInDB.getPwd();
if(pwd.equals(pwdInDB)){//验证密码
return true;
}
System.out.println("您输入的密码不正确!");
}
return false;
}
static{
for(int i = 0;i<10;i++){
UserInfo user = new UserInfo();
user.setName("user"+i);
user.setPwd("pwd"+i);
userDB.put(user.getName(), user);
}
}
}
这里用到静态块,将预设的用户名和密码保存到一个map中,在加载java类的时候,首先执行类里面的static代码块,然后进入main入口函数,因此在验证方法被调用前,map中就有了预设的用户信息,可以验证用户登录。