客户端 、 服务器
客户端连接到一个服务器上后就可以向服务器发送信息,服务器接受到信息之后会将信息馈送给连接在这个服务器上的所有客户端。
1.客户端
-
建立socket连接
客户端和服务器各有对应的IP地址以及TCP端口号,但客户端只需要知道服务器的端口就可以完成收发消息的任务了。其中TCP端口号是一个16位数字(0~65535);(0~1023)保留给已知服务,不能使用;(1024~65535)可以给服务器使用。
下面是客户端连接到服务器的代码:
-
读取socket
下面的代码展示了客户端如何接受服务器发送的信息,读取过程和上一章类似,建立串流并使用BufferedReader从socket上读取数据:
//建立连接,127.0.0.1这个地址是本机,即在本机上同时测试客户端和服务器端(socket内的数据为字节)
Socket chatScoket = new Socket("127.0.0.1", 5000);
//从socket取得输入串流(InputStreamReader内的数据转化成了字符)
InputStreamReader stream = new InputStreamReader(chatScoket.getInputStream());
//建立BufferedReader来读取
BufferedReader reader = new BufferedReader(stream);
String message = reader.readLine();
- 写数据到socket
PrintWriter可以将字符串数据转化为socket所需要的字节数据。不同于上面使用缓冲区读取数据,在写数据时每次写入一个string。下面的代码展示了如何使用PrintWriter来向socket写入数据:
//连接
Socket chatSocket = new Socket("127.0.0.1", 5000);
//创建连接到socket的PrintWriter,可以将字符串数据转化为socket所需要的字节数据
PrintWriter writer = new PrintWriter(chatSocket.getOutputStream());
writer.println("message to send"); //最后有换行
writer.print("another message"); //无换行
2.服务器
服务器需要一对socket(ServerSocket和Socket),其中前者是等待监听用户请求,后者用来与用户通信:
try {
//创建ServerSocket用来让服务器监听来自4242端口的用户请求。
ServerSocket serverSock = new ServerSocket(4242);
while(true) {
Socket sock = serverSock.accept(); //接受客户端的sock
PrintWriter writer = new PrintWriter(sock.getOutputStream()); //用PrintWriter写入
String advice = getAdvice(); //外部函数,可以获得一个字符串
writer.println(advice);
writer.close(); //送出之后就需要关闭
}
} catch(IOException ex) {
ex.printStackTrace();
}
总结:下面是完整的客户端与服务器端的代码,先运行服务器,然后运行客户端,即可接受从服务器端发出的信息。
客户端代码:
public class DailyAdviceClient {
public static void main(String[] args) {
DailyAdviceClient client = new DailyAdviceClient();
client.go();
}
public void go() {
try {
Socket s = new Socket("127.0.0.1", 4242);
InputStreamReader streamReader = new InputStreamReader(s.getInputStream());
BufferedReader reader = new BufferedReader(streamReader);
String advice = reader.readLine();
System.out.println("Today you should:" + advice);
reader.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
服务器端代码:
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class DailyAdviceServer {
String[] adviceList = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
public void go() {
try {
//创建ServerSocket用来让服务器监听来自4242端口的用户请求。
ServerSocket serverSock = new ServerSocket(4242);
while(true) {
Socket sock = serverSock.accept(); //接受客户端的sock
PrintWriter writer = new PrintWriter(sock.getOutputStream());
String advice = getAdvice();
writer.println(advice);
writer.close();
System.out.println(advice);
}
} catch(IOException ex) {
ex.printStackTrace();
}
}
private String getAdvice() {
int random = (int) (Math.random() * adviceList.length);
return adviceList[random];
}
public static void main(String[] args) {
DailyAdviceServer server = new DailyAdviceServer();
server.go();
}
}
多线程
上面搭建了一个简单的客户端服务器用于传输信息,但是它只能服务于一个用户,即在没有完成目前用户响应之前,无法进入accept()等待接受其他用户的请求,为了解决这个问题,在此引入多线程。
Thread
正常情况下,我们调用类的main函数则表示一个线程,假如我们在调用main的同时还想调用这个类的其他函数,则需要创建一个新的线程。一个程序的多个线程是遵循Java虚拟机的线程调度机制(时分),即每个线程轮流执行一个极短的时间。
那么多线程如何实现呢?
- 创建另一个线程
需要建立一个Runnable对象(是一个接口,代表线程的任务),Thread(执行工人),然后启动:
Runnable threadJob = new MyRunnable();
Thread myThread = new Thread(threadJob);
myThread.start();
- 实现Runnable接口
public class MyRunnable implements Runnable {
public void run() {
//需要执行的任务
}
}
小知识点:
- 线程有三种状态:可执行、执行中、被锁住。(操作系统的知识点)
- Thread.sleep(int):在指定沉睡时间之前,线程一定不会被唤醒。此外sleep()可能会抛出InterruptedException错误,需要对其进行异常处理。
- Thread对象不能被重复使用,一旦线程的run()方法完成之后,这个对象就没有意义了。
- Thread.setName(“xxx”):可以给线程取名,用来进行区分。
并发性问题
由于多线程共享对象,并且线程执行的顺序是不确定的,在多线程调用改动对象的数据时可能会导致错误的发生,这种现象被称为并发性问题。
synchronized(同步化)
使用synchronized关键词将作用在数据上的方法进行同步化,形象的来说是让这个方法变成一个“原子”,或者给这个方法上一个“锁”,并且这个“锁”只有一把钥匙。
一旦线程进入了同步化后的方法,则其他线程必须在这个线程完成方法里面的所有步骤之后才可以进入。
局部同步化操作如下:
另外,同步化也会带来新的问题:死锁。(两个线程+两个对象)就可能发生死锁。Java没有处理死锁的机制,需要在编写多线程程序的过程中避免出现死锁情况。
单服务器多客户端的简单并发多线程聊天程序
来自15章最后,其中有个小坑就是JTextArea的长度设置为50,如果你打开的聊天框很小的话,会有部分内容无法显示,可以手动拉长聊天框或者把这个值设置低一些。
服务器端:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;
import java.util.ArrayList;
import java.util.Iterator;
//主线程用来和客户端进行连接,创建了另外的线程把某用户发送的消息发给所有用户。
public class VerySimpleChatServer {
ArrayList clientOutputStreams; //用来保留所有用户的writer,下面发送信息时会遍历所有用户
public static void main(String[] args) {
new VerySimpleChatServer().go();
}
public void go() {
clientOutputStreams = new ArrayList();
try {
ServerSocket serverSocket = new ServerSocket(5000);
while(true) {
Socket clientSocket = serverSocket.accept();
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
clientOutputStreams.add(writer);
Thread t = new Thread(new ClinetHandler(clientSocket));
t.start();
System.out.println("got a connection");
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void tellEveryone(String message) {
Iterator it = clientOutputStreams.iterator();
while(it.hasNext()) {
try {
PrintWriter writer = (PrintWriter) it.next();
writer.println(message);
writer.flush();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
public class ClinetHandler implements Runnable {
BufferedReader reader;
Socket sock;
public ClinetHandler(Socket clinetSocket) {
try {
sock = clinetSocket;
InputStreamReader isReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader((isReader));
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void run() {
String message;
try {
while ((message = reader.readLine()) != null) {
System.out.println("read " + message);
tellEveryone(message);
}
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
}
客户端:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
public class SimpleChatClientB {
JTextArea incoming; //显示信息文本域
JTextField outgoing; //输入文本框
BufferedReader reader;
PrintWriter writer;
Socket sock;
//内部包含两个线程,主线程是监听按钮发送信息给服务器,我们创建了另一个线程来接受新的信息并显示
public void go() {
JFrame frame = new JFrame("Ludicrously Simple Chat ClientB");
JPanel mainPanel = new JPanel();
incoming = new JTextArea(15,50);
incoming.setLineWrap(true);
incoming.setWrapStyleWord(true);
incoming.setEditable(false);
JScrollPane qScroller = new JScrollPane(incoming); //滚动条?
qScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
qScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
outgoing = new JTextField(20);
JButton sendButton = new JButton("Send");
sendButton.addActionListener(new SendButtonListener());
mainPanel.add(qScroller);
mainPanel.add(outgoing);
mainPanel.add(sendButton);
setUpNetworking();
// 启动新的线程,以内部类作为任务,读取服务器的socket串流,显示在本文区域
Thread readerThread = new Thread(new IncomingReader());
readerThread.start();
frame.getContentPane().add(BorderLayout.CENTER, mainPanel);
frame.setSize(400,500);
frame.setVisible(true);
}
//通过socket创建好输入输出需要用的串流
private void setUpNetworking() {
try {
sock = new Socket("127.0.0.1", 5000);
//输入读取流
InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader(streamReader);
//输出写入流
writer = new PrintWriter(sock.getOutputStream());
System.out.println("networking established");
} catch (IOException ex) {
ex.printStackTrace();
}
}
public class SendButtonListener implements ActionListener {
public void actionPerformed(ActionEvent ev) {
try {
writer.println(outgoing.getText());
writer.flush();
} catch (Exception ex) {
ex.printStackTrace();
}
outgoing.setText("");
outgoing.requestFocus();
}
}
//thread的任务
public class IncomingReader implements Runnable {
public void run() {
String message;
try {
while((message = reader.readLine()) != null) { //代表有新数据可以读取
System.out.println("read " + message);
incoming.append(message+ "\n");
incoming.paintImmediately(incoming.getBounds());
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
SimpleChatClientB client = new SimpleChatClientB();
client.go();
}
}