网络通信的本质是网络间的数据 IO。
Socket通信方式:
有连接(TCP)和无连接(UDP数据报)
有连接的通信过程: 需要到一个重要的类Socket类,Socket类每new一次会有产生两个对象(可以简单理解成生活中的两个好基友的通话,但是是事先约定好了一个人(服务器Server端)先在听,也就是一直等待着他的基友来呼叫他,他的基友(客户端Client端)呼叫,当他收到他的基友的呼叫时,就也生成一个Socket对象(注意:服务器Server端的这个Socket对象不是new出来的,而是用一个监听的方法 accept()来得到。),然后两个人就可以通话了。)
建立的过程如下图。
实际上我们建立通信是在两台电脑的两个程序之间,所以知道IP地址还不够,IP地址只是每台电脑的一个身份证号码,但我们要如何在电脑运行着的多个程序中找到需要建立通信的那个程序呢?那就要通过端口号了。
这样也就可以理解new Socket对象时需要的两个参数(IP地址和端口号)了。
连接建立之后,两个相关联的Socket对象之间就可以相互发消息了。而发送消息可读取消息就要用到 IO流了。
但Socket无法得到字符流,所以要引进两个类来把字节流转化成字符流。
实现到客户端验证登录
我们每天都在使用的QQ,微信登录都要经过一个 1.客户端发用户名和密码到服务器验证,2.服务器进行验证,然后3.服务器将验证结果返回给客户端,4.客户端根据返回结果决定显示主界面。
存在问题:服务器启动之后,一个客户端登录验证后,就不能再登录另一个客户端了?
因为上一个客户端还没发送完消息,它还在占用着这个通信的管道,所以第二个客户端只能等待,或者要重新开启服务器。但这样也是只能一次只为一人服务。其实这就是IO中的阻塞了。上图中建立通信过程中accept()方法会阻塞。
为什么 accept()方法为什么会阻塞?
因为服务器线程发起一个 accept 动作,询问操作系统是否有新的 socket 信息从端口 X 发送过来。注意这里是询问操作系统。如果操作系统没有发现有 socket 从指定的端口 X 来,那么操作系统就会等待。这样 serverSocket.accept()方法就会一直等待,就造成了阻塞。
要解决这个问题就要使用多线程了。 为每一个客户端连接请求都创建一个线程来单独处理。
客户端的Socket对象创建在QQLogin中,要在QQMain中使用,该怎么解决?使用set方法。
//登录界面
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.* ;
import java.net.* ;
public class QQLogin extends JFrame implements ActionListener {
JTextField txtUser = new JTextField() ;
JPasswordField txtPass = new JPasswordField() ;
public QQLogin() {
this.setSize(250 , 125) ;
//new组件
JLabel labUser = new JLabel("用户名") ;
JLabel labPass = new JLabel("密码") ;
JButton btnLogin = new JButton("登陆") ;
JButton btnReg = new JButton("注册") ;
JButton btnCancel = new JButton("取消") ;
//注册事件监听
btnLogin.addActionListener(this) ;
btnReg.addActionListener(this) ;
btnCancel.addActionListener(this) ;
//布置输入面板
JPanel panInput = new JPanel() ;
panInput.setLayout(new GridLayout(2 , 2)) ;
panInput.add(labUser) ;
panInput.add(txtUser) ;
panInput.add(labPass) ;
panInput.add(txtPass) ;
//布置按钮面板
JPanel panButton = new JPanel() ;
panButton.setLayout(new FlowLayout()) ;
panButton.add(btnLogin) ;
panButton.add(btnReg) ;
panButton.add(btnCancel) ;
//布置窗体
this.setLayout(new BorderLayout()) ;
this.add(panInput , BorderLayout.CENTER) ;
this.add(panButton , BorderLayout.SOUTH) ;
}
public static void main(String args[]){
QQLogin w = new QQLogin() ;
w.setVisible(true) ;
}
@Override
public void actionPerformed(ActionEvent arg0) {
if(arg0.getActionCommand().equals("登陆")){
try {
// 发送用户名和密码到服务器
String user = txtUser.getText();
String pass = txtPass.getText();
Socket s = new Socket("127.0.0.1" , 8000) ;
OutputStream os = s.getOutputStream() ;
OutputStreamWriter osw = new OutputStreamWriter(os) ;
PrintWriter pw = new PrintWriter(osw , true) ;
pw.println(user+"%"+pass) ;
//接受服务器发送回来的确认信息
InputStream is = s.getInputStream() ;
InputStreamReader isr = new InputStreamReader(is) ;
BufferedReader br = new BufferedReader(isr) ;
String yorn = br.readLine() ;
//显示主窗体
if (yorn.equals("ok")) {
QQMain w = new QQMain();
w.setSocket(s);
w.setVisible(true);
this.setVisible(false);
}else {
JOptionPane.showMessageDialog(this, "对不起,用户名或密码错误") ;
}
} catch (Exception e) {}
}
if(arg0.getActionCommand().equals("注册")){
System.out.println("用户点了注册") ;
}
if(arg0.getActionCommand().equals("取消")){
System.out.println("用户点了取消") ;
}
}
}
//发消息界面
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.* ;
public class QQMain extends JFrame implements ActionListener{
private Socket s;
public void setSocket(Socket s){
this.s=s;
}
JTextField txtMess = new JTextField() ;
JComboBox cmbUser = new JComboBox() ;
JTextArea txtContent = new JTextArea() ;
QQMain(){
this.setSize(300 , 400) ;
//new组件
JButton btnSend = new JButton("发送") ;
JScrollPane spContent = new JScrollPane(txtContent) ;
//注册事件监听
btnSend.addActionListener(this) ;
//布置小面板
JPanel panSmall = new JPanel() ;
panSmall.setLayout(new GridLayout(1 , 2)) ;
panSmall.add(cmbUser) ;
panSmall.add(btnSend) ;
//布置大面板
JPanel panBig = new JPanel() ;
panBig.setLayout(new GridLayout(2 , 1)) ;
panBig.add(txtMess) ;
panBig.add(panSmall) ;
//布置窗体
this.setLayout(new BorderLayout()) ;
this.add(panBig , BorderLayout.NORTH) ;
this.add(spContent , BorderLayout.CENTER) ;
//读聊天记录
try{
File f = new File("c:/work/聊天记录.qq") ;
FileReader fr = new FileReader(f) ;
BufferedReader br = new BufferedReader(fr) ;
while(br.ready()){
txtContent.append(br.readLine()+"\n") ;
}
}catch(Exception e){}
}
@Override
public void actionPerformed(ActionEvent arg0) {
// txtMess -------> txtContent
txtContent.append(txtMess.getText()+"\n") ;
// 将txtMess的内容存入聊天记录文件
try{
File f = new File("c:/work/聊天记录.qq") ;
FileWriter fw = new FileWriter(f) ;
PrintWriter pw = new PrintWriter(fw) ;
pw.println(txtMess.getText()) ;
pw.close() ;
}catch(Exception e){}
//发送消息到服务器
try{
OutputStream os=s.getOutputStream();
OutputStreamWriter osw=new OutputStreamWriter(os);
PrintWriter pw=new PrintWriter(osw,true);
pw.print(txtMess.getText());
//System.out.println(txtMess.getText());
}catch(Exception e){}
// 清除txtMess中的内容
txtMess.setText("") ;
}
}
//服务器端
import java.net.*;
import java.io.*;
public class QQServer {
public static void main(String[] args) {
try {
// 服务器在8000端口监听
ServerSocket ss = new ServerSocket(8000);
System.out.println("服务器正在8000端口监听......");
Socket s = ss.accept();
// 接受用户名和密码
InputStream is = s.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String uandp = br.readLine();
// 拆分用户名和密码
String u = " ";
String p = " ";
try {
u = uandp.split("%")[0];
p = uandp.split("%")[1];
} catch (Exception ee) {
}
// 服务器发送信息
OutputStream os = s.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
PrintWriter pw = new PrintWriter(osw, true);
if (u.equals("aaa") && p.equals("111")) {
// 发送正确信息到客户端
pw.println("ok");
while (true) {
String message = br.readLine();
System.out.println(message);
}
} else {
// 发送错误信息到客户端
pw.println("err");
}
} catch (Exception e) {
}
}
}