用JAVA实现简单的客户端-服务器通信
1、客户端-服务器模型
我们每天都在上网,聊QQ、聊微信、看视频、看新闻......基本每个网络应用都是基于客户端-服务器模型的,在这个模型下,一个应用就是一个服务器进程和多个客户端进程组成。服务器进程启动之后,就开始等待客户端连接,当服务器与客户端建立连接后,服务器又等待客户端发出服务请求,客户端通过这个连接向服务器发送服务请求,最后服务器利用其现有资源满足客户端的请求并发给客户一个响应,然后等待下一个请求,从客户端向服务器发出请求到服务器处理请求这一系列的过程称为一个事务。
值得注意的是,服务器和客户端应该理解为进程(而不是机器),一台机器可以有多个服务器或客户端,一般客户端与服务器在不同的机器上,但也可以在同一台机器上。
2、套接字
客户端和服务器之间要进行通信首先要建立连接,然后才能在这个连接上收发字节流。
套接字就是连接的一个端点,每个套接字都有一个套接字地址,由32位整数IP地址和16位整数端口号组成,用“IP地址:端口号”表示。这就好比要在宿舍楼找到一个人,不仅要知道他的楼栋号(IP),还要知道房间号(端口)。
一个连接由两端的套接字地址唯一确定,称为套接字对。
(client address: client port, server address: server port)
其中客户端的端口是临时分配的端口,服务器的端口是固定的知名端口。
Java中的服务器套接字是ServerSocket,客户端套接字是socket。两者建立连接后,就能通过输入输出流进行通信了(服务器的输入流对应客户端的输出流,服务器的输出流对应客户端的输入流)。
3、用JAVA实现简单服务器端
服务器界面类:Server,输入端口号,通过点击按钮创建服务器。
public class Server {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Server svr=new Server();
svr.ini();
}
/**
* @Title: ini
* @Description: 服务器界面,在上面输入端口号则建立一个服务器线程(利用线程可以同时创建多个端口的服务器)
*/
public void ini(){
int port;
JFrame jf=new JFrame();
jf.setSize(new Dimension(500,500));
jf.setLocationRelativeTo(null);
jf.setResizable(false);
BorderLayout bo=new BorderLayout();
jf.setLayout(bo);
JPanel jpa=new JPanel();
jpa.setBackground(Color.white);
JTextField jtfPort=new JTextField();
jtfPort.setPreferredSize(new Dimension(100,20));
JButton jbuCreat=new JButton();
jbuCreat.setPreferredSize(new Dimension(50,20));
ActionListener cbl=new CreatBtnListener(jtfPort);
jbuCreat.addActionListener(cbl);
jpa.add(jtfPort);
jpa.add(jbuCreat);
jf.add(jpa,BorderLayout.CENTER);
jf.setVisible(true);
}
}
创建服务器的按钮监听器类:CreatBtnListener
public class CreatBtnListener implements ActionListener{
private int port;//
private JTextField jtf;
public CreatBtnListener(JTextField jtf){
this.jtf=jtf;
}
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
port=Integer.parseInt(jtf.getText());
new ChatServerThread(port).start();
}
}
创建服务器的线程ChatServerThread
public class ChatServerThread extends Thread{
private int port;
private JTextField jtf;
public ChatServerThread(int port){
this.port=port;
}
public void run(){
this.creatServer(port);
}
public void creatServer(int port){
try {
//创建服务器,服务器套接字ServerSocket
ServerSocket server=new ServerSocket(port);
System.out.println("服务器创建成功"+",端口号:"+port);
//服务器Socket
while(true){//此循环是为了不断接受客户机连接
Socket client=server.accept();//代表客户机和服务器的连接
new ProcessClientThread(client).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
服务器应具有同时处理多个客户端事务的能力,因此把对每一个连接上的客户端的处理方法封装在线程中
public class ProcessClientThread extends Thread{
private Socket client;
private OutputStream os;
public ProcessClientThread(Socket client){
this.client=client;
System.out.println("创建服务器线程");
}
public void run(){
processClient(client);
System.out.println("服务器线程启动");
System.out.println("client="+client.getRemoteSocketAddress());
}
public void sendMsg2Me(String msg){
try {
msg=msg+"\r\n";
os.write(msg.getBytes());
os.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void closeMe(){
try {
client.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @Title: readAll
* @param InputStream is
* @return String
* @Description: 直接读字节流,因为BufferedReader的ReadLine()方法以回车换行作为结束符,而登陆界面的用户名、
* 密码是不含回车换行的,这个方法主要是为了读取客户端界面的JTextField中的账号和密码
*/
public String readAll(InputStream is){
try {
ArrayList al=new ArrayList();
int i=0;
do{//这里只能用do-while循环而不能用while,否则读不到任何数据
i=is.read();
al.add(i);
}while(is.available()!=0);
byte[] b=new byte[al.size()];
for(int j=0;j<b.length;j++){
int s=(int)al.get(j);//get返回的是Object类对象
b[j]=(byte)s;
}
return new String(b);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public void processClient(Socket client){
// System.out.println("客户机"+client.getRemoteSocketAddress()+"已连接上");
//获取客户机的输入输出流,这里文件对象看成客户机的Socket
try {
os = client.getOutputStream();//输出到客户机的输出流
InputStream is=client.getInputStream();//从客户机输入的输入流
BufferedReader brd=new BufferedReader(new InputStreamReader(is));
System.out.println(brd);
String userName=readAll(is);
String psw =readAll(is);
UserInfo user=new UserInfo();
user.setName(userName);
user.setPsw(psw);
System.out.println("用户名"+userName);
System.out.println("密码"+psw);
//验证
boolean loginState=DaoTools.checkUser(user);
if(!loginState){//不存在则关闭
// sendMsg2Me("用户名或密码错误");
// this.closeMe();
os.write(0);
os.flush();
return;
}
os.write(1);
os.flush();
ChatTools.addClient(this);//登陆成功
ChatTools.loginMsg(userName);
String msg=brd.readLine();
while(!msg.equals("bye")&&!msg.equals("Bye")&&!msg.equals("BYE")){
ChatTools.castMsg(user, msg);
msg=brd.readLine();//阻塞
}
ChatTools.removeClient(this);
ChatTools.logoffMsg(userName);
os.write("exit".getBytes());
is.close();
os.close();
this.closeMe();
// client.close();//??
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
三个后台辅助类UserInfo,DaoTools,ChatTools.
UserInfo作为客户的抽象,DaoTools用来维护用户的后台信息,ChatTools用来维护现在正在处理客户事务的所有线程
/**
* 用户信息类,一个实例代表一个用户,包含用户名密码
*/
public class UserInfo {
private String name;//用户名
private String psw;//密码
private String loginTime;//上线时间
private String address;//上线地址
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPsw() {
return psw;
}
public void setPsw(String psw) {
this.psw = psw;
}
public String getLoginTime() {
return loginTime;
}
public void setLoginTime(String loginTime) {
this.loginTime = loginTime;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
/**
* 数据访问类,data access object
* 保存用户信息,用户登录验证,对用户进行增加(Create)、读取(Retrieve)(重新得到数据)、更新(Update)和删除(Delete)操作,CRUD操作
*/
public class DaoTools {
public static boolean checkUser(UserInfo user){
if(userDB.containsKey(user.getName())){
UserInfo user0=userDB.get(user.getName());
if(user0.getPsw().equals(user.getPsw()))
return true;
}
System.out.println("用户名或密码错误"+user.getName());
return false;
}
// public static boolean checkPsw(UserInfo user){
//
// }
private static Map<String,UserInfo> userDB=new HashMap();
static {
for (int i=0; i<10;i++){
UserInfo user=new UserInfo();
user.setName("user"+i);
user.setPsw("user"+i);
userDB.put(user.getName(),user);
}
}
}
/**
* 负责维护客户线程的类
* 类中的方法都是静态的
*/
public class ChatTools {
private ChatTools(){};
//保存线程处理的队列对象
private static List<ProcessClientThread> clientList=new ArrayList<>();
public static void addClient(ProcessClientThread pct){
clientList.add(pct);
}
public static void removeClient(ProcessClientThread pct){
clientList.remove(pct);
}
public static void castMsg(UserInfo sender,String msg){
String name=sender.getName()+":";
for(int i=0;i<clientList.size();i++){
ProcessClientThread client=clientList.get(i);
client.sendMsg2Me(name);
}
for(int i=0;i<clientList.size();i++){
msg=" "+msg;
ProcessClientThread client=clientList.get(i);
client.sendMsg2Me(msg);
}
}
public static void castMsg(String msg){
for(int i=0;i<clientList.size();i++){
ProcessClientThread client=clientList.get(i);
client.sendMsg2Me(msg);
}
}
public static void loginMsg(String userName){
castMsg(userName+"上线了,当前人数为"+clientList.size());
}
public static void logoffMsg(String userName){
castMsg(userName+"下线了,当前人数为"+clientList.size());
}
}
服务器界面
4、实现客户端
客户端套接字使用Socket类,
(1)客户端开始界面,Start类
在开始界面中,添加服务器IP和端口号的文本输入框,客户端启动按钮。然后给按钮加监听器ClientListener,监听器中建立“服务器-客户端”连接,并跳出登陆界面
public class Start {
/**
* @Title: main
* @param args
* @return void
* @Description: TODO
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Start s=new Start();
s.ini();
}
public void ini(){
JFrame jf=new JFrame();
JPanel jp=new JPanel();
JTextField textIP=new JTextField();
JTextField textPort=new JTextField();
textIP.setText("localhost");
textPort.setText("9091");
JButton btn=new JButton("连接");
JLabel ipJLA=new JLabel("IP:");
JLabel portJLA=new JLabel("PORT:");
btn.setPreferredSize(new Dimension(50,30));
jf.setSize(new Dimension(500,300));
BorderLayout bla=new BorderLayout();
jf.setLayout(bla);
jp.setBackground(Color.white);
jp.setLayout(null);
jp.add(ipJLA);
jp.add(portJLA);
jp.add(textIP);
jp.add(textPort);
jp.add(btn);
ipJLA.setBounds(50, 30, 50, 30);
portJLA.setBounds(30, 65, 50, 30);
textIP.setBounds(65,30 , 300, 30);
textPort.setBounds(65,65 , 300, 30);
btn.setBounds(100,100,100,30);
ActionListener act=new ClientListener(textIP,textPort);
btn.addActionListener(act);
jf.add(jp);
jf.setVisible(true);
}
}
客户端界面
(2)ClientListener监听器。内容包括登陆界面,登陆成功后的聊天界面,实质的数据接收和发送,数据显示。登陆功能通过启动另一个监听器实现
/**
* 读取从服务器发来的消息
* 把消息发给服务器
*/
public class ClientListener extends Thread implements ActionListener{
private JTextField ipField;
private JTextField portField;
private Socket client;
private String addr;
private int port;
private InputStream is;
private OutputStream os;
private JFrame chatF;
private JTextArea sendArea;
private JTextArea chatArea;
public ClientListener(JTextField ipField,JTextField portField){
this.ipField=ipField;
this.portField =portField;
}
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
addr=ipField.getText();
port=Integer.parseInt(portField.getText());
this.connect(addr, port);
logFrame();
}
public void run(){
String msg;
while(true){
msg=receiveMsg();
displayMsg(msg+"\r\n");
}
}
public void logFrame(){
JFrame logF=new JFrame();
logF.setSize(800, 500);
BorderLayout bo=new BorderLayout();
logF.setLayout(bo);
JPanel jpa=new JPanel();
jpa.setLayout(null);
jpa.setBackground(Color.black);
JTextField jtx1=new JTextField();
JTextField jtx2=new JTextField();
JButton btn=new JButton();
jpa.add(btn);
jpa.add(jtx1);
jpa.add(jtx2);
jtx1.setBounds(100,40, 200, 40);
jtx2.setBounds(100,100, 200, 40);
btn.setBounds(120, 200, 100, 50);
btn.addActionListener(new LoginListener(jtx1,jtx2,os,is,this));
// chatFrame();
logF.add(jpa);
logF.setVisible(true);
}
public void chatFrame(){
chatF=new JFrame();
chatF.setSize(new Dimension(400,600));
BorderLayout bod=new BorderLayout();
chatF.setLayout(bod);
JPanel jpa=new JPanel();
chatF.add(jpa);
jpa.setBackground(Color.black);
sendArea=new JTextArea();
chatArea=new JTextArea();
JButton sendBtn=new JButton("发送");
jpa.setLayout(null);
jpa.add(chatArea);
jpa.add(sendArea);
jpa.add(sendBtn);
chatArea.setBounds(10, 10, 270, 320);
sendArea.setBounds(10, 340, 270, 180);
sendBtn.setBounds(270, 540, 40, 30);
sendBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
try {
os.write((sendArea.getText()+"\r\n").getBytes());
sendArea.setText("");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
chatF.setVisible(true);
this.start();
}
/**
* @Title: connect
* @param address 服务器ip
* @param port 要连接的端口
* @return void
* @Description: 连接到服务器
*/
public void connect(String address,int port){
try {
client= new Socket(address,port);
is=client.getInputStream();
os=client.getOutputStream();
System.out.println("连接成功");
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @Title: sendMsg2
* @param msg 要发送的消息
* @Description: 发送消息给服务器
*/
public void sendMsg2(String msg){
try {
os.write(msg.getBytes());
os.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public String receiveMsg(){
BufferedReader brd =new BufferedReader(new InputStreamReader(is));
String msg=null;
try {
msg = brd.readLine();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return msg;
}
/**
* @Title: displayMsg
* @Description: 显示
*/
public void displayMsg(String msg){
chatArea.append(msg);
}
}
登 陆 界 面
(3)登陆监听器LoginListener,把登陆界面输入的内容用输出流发送给服务器(服务器端用相应的输入流读取)。在启动LoginListener时,把调用此监听器的ClientListener的应用传进来,以便登陆成功时直接启动ClientListener中进入聊天界面的方法ChatFrame()。传引用类似于C/C++中的传指针,不属于值传递,其指向的是内容没有改变,对改引用进行的操作实际上就是对原对象的操作,因为指向的都是同一片内存空间,同时也不用担心会增加很多内存花销。
public class LoginListener implements ActionListener{
private JTextField jtx1;
private JTextField jtx2;
private OutputStream os;
private InputStream is;
private ClientListener cl;
public LoginListener(JTextField jtx1,JTextField jtx2,OutputStream os,InputStream is,ClientListener cl){
this.jtx1=jtx1;
this.jtx2=jtx2;
this.os=os;
this.is=is;
this.cl=cl;
}
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
try {
os.write((jtx1.getText()).getBytes());
System.out.println("jtx1="+jtx1.getText());
os.flush();
os.write(jtx2.getText().getBytes());
System.out.println("jtx1="+jtx2.getText());
os.flush();
int flag=-1;
System.out.println("flag="+flag);
try {
flag = is.read();
System.out.println("读了");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
if(flag==1){
cl.chatFrame();
}
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
客户端聊天界面(右下角是发送按钮)
(界面是随便做的,略丑)