多客户机聊天程序编程
思路就是将服务器为一个个客户机的请求创建一个个对应的 socket 接口进行处理,由于 ServerSocket 里面的 accept() 会在没有客户机请求的时候一直处于等待状态,所以非常合适作为创建线程的条件
客户机代码
import java.io.*;
import java.net.*;
public class MultiTalkClient {
public static void main(String[] args) {
try{
Socket socket = new Socket("127.0.0.1", 4888);
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
PrintWriter os = new PrintWriter(socket.getOutputStream());
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readline;
readline = sin.readLine();
while(!readline.equals("bye")){
os.println(readline);
os.flush();
System.out.println("Client: "+readline);
System.out.println("Server: "+is.readLine());
readline = sin.readLine();
}
os.close(); is.close(); sin.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
这边和上一篇的 Client 程序几乎没有区别
服务器主代码:
import java.io.*;
import java.net.*;
public class MultiTalkServer {
static int clientnum = 0;
public static void main(String[] args) {
ServerSocket serverSocket = null;
boolean listening = true;
try{
serverSocket = new ServerSocket(4888);
while(listening){
new ServerThread(serverSocket.accept(), clientnum).start();
clientnum++;
}
serverSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
服务器创建线程代码
import java.io.*;
import java.net.*;
public class ServerThread extends Thread{
Socket socket = null;
int clientnum;
public ServerThread(Socket socket, int num){
this.socket = socket;
clientnum = num+1;
}
@Override
public void run() {
try{
String line;
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter os = new PrintWriter(socket.getOutputStream());
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Client"+clientnum+": "+is.readLine());
line = sin.readLine();
while(!line.equals("bye")){
os.println(line);
os.flush();
System.out.println("Server:"+line);
System.out.println("Client"+clientnum+": "+is.readLine());
line = sin.readLine();
}
os.close(); is.close(); sin.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
但是这个程序有点反智,总之能一句一句交流,但是不能指定和谁交流,如果要达到服务器能和多个客户机有差别地随意交流。首先要设计成上一篇的那种每个机器一个接收键盘输入的信息然后输出给对方,一个用来接收对方传过来的信息。然后要实现去中心化,将每个 server-client 对的线程捆绑创建,而不是有一个 server 的主线程用来接收所有的键盘输入,这样不适合这种多对多的交流,会使得交流变得混乱。所以应该将 server 线程也做成对每个 client 线程一个独立的程序进行响应。
数据报通信
- UDP(User Datagram Protocal) 提供的是非面向连接的不可靠数据包传输,相关的类有 DatagramPacket,DatagramSocket,MulticastSocket 等。
- TCP(Transport Control Protocal) 提供的是面向连接的可靠数据传输协议,URL,URLConnection,Socket,ServerSocket 等类都是使用 TCP 协议进行网络通讯。
UDP 传输有大小限制,每个包的大小在 64K 之内。(长度字段为16位)
DatagramSocket 的构造方法:
- DatagramSocket()
- DatagramSocket(int port)
DatagramPacket 的构造方法:
- DatagramPacket(byte ibuf[], int ilength) // 用于接收数据报
- DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport) // 用于发送数据报
数据报通信
收数据报:
DatagramPacket packet = new DatagramPacket(buf, 256);
socket.receive(packet);
发送数据报:
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
服务器可以使用接收数据报 DatagramPacket 的 getAddress() 和 getPort() 方法来获取获得客户机的 ip 地址和端口号,然后就可以创建新的 DatagramPacket 用于发送。而之前 TCP 通信中,客户机的 Socket 建立连接的时候,就会将自己的 ip 地址和端口号发送出去,然后serverSocket 的 accept() 方法接收,返回 Socket 对象的时候就会包含将客户机的 ip 地址和 port。
实现服务器向客户机发送信息的一个简单的例子:
客户机:
import java.io.*;
import java.net.*;
public class QuoteClient {
public static void main(String[] args) {
if(args.length!=1){
System.out.println("Please input hostname");
return ;
}
try{
DatagramSocket socket = new DatagramSocket();
byte[] buf = new byte[256];
InetAddress address = InetAddress.getByName(args[0]);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4445);
socket.send(packet);
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData());
System.out.println("Quote of the Moment: "+received);
System.out.println(new String(buf));
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
服务器:
import java.io.*;
import java.net.*;
import java.util.*;
public class QuoteServerThread extends Thread{
protected DatagramSocket socket = null;
protected BufferedReader in = null;
protected boolean moreQuotes = true;
public QuoteServerThread() throws IOException {
this("QuoteServerThread");
}
public QuoteServerThread(String name) throws IOException{
super(name);
socket = new DatagramSocket(4445);
try{
in = new BufferedReader(new FileReader("./doc/the-sonnets-part1.txt"));
}catch (FileNotFoundException e){
e.printStackTrace();
}
}
@Override
public void run() {
try{
byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String dString = null;
if(in==null){
dString = new Date().toString();
}
else
dString = getNextQuote();
buf = dString.getBytes();
InetAddress address = packet.getAddress();
int port = packet.getPort();
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
}catch (IOException e){
e.printStackTrace();
moreQuotes = false;
}
socket.close();
}
protected String getNextQuote(){
String returnValue = null;
try{
if((returnValue=in.readLine())==null){
in.close();
moreQuotes = false;
returnValue = "No more quotes. Goodbye!";
}
}catch (IOException e){
returnValue = "IOException occurred in server!";
}
return returnValue;
}
}
服务器线程启动程序:
import java.io.*;
public class QuoteServer {
public static void main(String[] args) throws IOException{
new QuoteServerThread().start();
}
}
运行的时候回给 Client 返回文件第一行的内容,如果没有这个文件的时候就会返回当前日期。
输出证明接收的 DatagramPacket 的 getData() 返回的内容和里面的参数 buf 里的参数相同。
运用数据报进行广播通信
- DatagramSocket 只允许把数据报发往一个目的地
- MulticastSocket 将数据报以广播方式发送到该连接到端口的所有用户
服务器向客户机广播一段内容给多个客户机
客户机程序:
import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastClient {
public static void main(String[] args) throws IOException {
MulticastSocket socket = new MulticastSocket(4446);
InetAddress address = InetAddress.getByName("230.0.0.1");
socket.joinGroup(address);
DatagramPacket packet;
for(int i=0; i<15; i++){
byte[] buf = new byte[256];
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData());
System.out.println("Quote of the moment: "+received);
}
socket.leaveGroup(address);
socket.close();
}
}
服务器处理程序:
import java.io.*;
import java.util.*;
import java.net.*;
public class MulticastServerThread extends QuoteServerThread {
private long HALF_SECOND = 500;
public MulticastServerThread() throws IOException{
super("MulticastServerThread");
}
@Override
public void run() {
while (moreQuotes){
try{
byte[] buf = new byte[256];
String dSting = null;
if(in==null) dSting = new Date().toString();
else dSting = getNextQuote();
buf = dSting.getBytes();
InetAddress group = InetAddress.getByName("230.0.0.1");
DatagramPacket packet = new DatagramPacket(buf, buf.length, group, 4446);
socket.send(packet);
try{
sleep((long)(Math.random()*HALF_SECOND));
}catch (InterruptedException e){e.printStackTrace();}
}catch (IOException e){
e.printStackTrace();
moreQuotes = false;
}
}
socket.close();
}
}
服务器创建线程程序:
import java.io.*;
public class MulticaseServer {
public static void main(String[] args) throws IOException {
new MulticastServerThread().start();
}
}
这里需要注意的是,以往的多端通信程序都是先启动服务器程序,这里我们先启动客户机程序。而且可以启动多个,然后当服务器程序启动之后,客户机程序下就会打印15行内容。
结合图形用户界面设计聊天程序
通过前面的学习,我们了解到:
- TCP 和 Socket 适合实现网络编程(多客户端)
- UDP 和 DatagramSocket 适合实现通信和广播,如在线直播时有很多观众的时候
以下介绍一个简易的图形用户界面展示的聊天程序
主要的代码:
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class ChatFrame extends JFrame implements ActionListener{
JTextField tf;
JTextArea ta;
JScrollPane sp;
JButton send;
JPanel p;
int port;
String s = null;
String myID;
Date date;
ServerSocket server;
Socket mySocket;
BufferedReader is;
PrintWriter os;
String line;
public ChatFrame(String ID, String remoteID, String IP, int port, boolean isServer){
super(ID);
myID = ID;
this.port = port;
ta = new JTextArea();
ta.setEnabled(false); // 不能手动编辑
sp = new JScrollPane(ta);
this.setSize(330, 400);
this.setResizable(false);
try{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}catch (Exception e){e.printStackTrace();}
this.getContentPane().add(sp, "Center"); // 将展示以往记录的区域放在中间
p = new JPanel();
this.getContentPane().add(p, "South");
send = new JButton("send");
tf = new JTextField(20);
tf.requestFocus(); // 输入框获取当前鼠标焦点
p.add(tf);
p.add(send); // 左边是输入框,右边是按钮
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setVisible(true);
send.addActionListener(this);
tf.addActionListener(this);
if(isServer){
try {
server = null;
try {
server = new ServerSocket(port); // 聊天接收方
} catch (Exception e) {
e.printStackTrace();
}
mySocket = null;
try {
mySocket = server.accept(); // 接收到一个想和他聊天的进程
} catch (Exception e) {
e.printStackTrace();
}
is = new BufferedReader(new InputStreamReader(mySocket.getInputStream()));
os = new PrintWriter(mySocket.getOutputStream());
}catch (Exception e){
e.printStackTrace();
}
}else{
try{
mySocket = new Socket(IP, port); // 聊天发起方
os = new PrintWriter(mySocket.getOutputStream());
is = new BufferedReader(new InputStreamReader(mySocket.getInputStream()));
}catch (Exception e){
e.printStackTrace();
}
}
while(true){
try{
line = is.readLine(); // 主线程负责接收对方的信息
date = new Date();
// 设置时间格式
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = format.format(date);
s += currentTime+" "+remoteID+" says:\n"+line+"\n";
ta.setText(s);
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public void actionPerformed(ActionEvent e) { // 事件处理线程用于处理本地输入
date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = format.format(date);
s += currentTime+" "+myID+" says:\n"+tf.getText()+"\n";
ta.setText(s);
os.println(tf.getText());
os.flush(); // 提交
tf.setText("");
tf.requestFocus(); // 获取当前鼠标焦点
}
}
服务器代码:
public class ChatServerFrame {
public static void main(String[] args) {
new ChatFrame("Cat", "Dog", "127.0.0.1", 8888, true);
}
}
客户机代码:
public class ChatClientFrame {
public static void main(String[] args) {
new ChatFrame("Dog", "Cat", "127.0.0.1", 8888, false);
}
}
先启动服务器后启动客户机的运行结果如下: