前言:需要对socket有简单的了解
IO模型主要分类
-
同步(synchronous) IO和异步(asynchronous)
-
IO阻塞(blocking) IO和⾮阻塞(non-blocking)IO
-
同步阻塞(blocking-IO)简称BIO
-
同步⾮阻塞(non-blocking-IO)简称NIO
-
异步⾮阻塞(synchronous-non-blocking-IO)简称AIO
同步:发送⼀个请求,等待返回,再发送下⼀个请求,同步可以避免出现死锁,脏读的发⽣。
异步:发送⼀个请求,不等待返回,随时可以再发送下⼀个请求,可以提⾼效率,保证并发。
BIO (同步阻塞I/O模式)数据的读取写⼊阻塞在⼀个线程内等待其完成。这⾥使⽤那个经典的烧开⽔例⼦,这⾥假设⼀个烧开⽔的场景,有⼀排⽔壶在烧开⽔,BIO的⼯作模式就是,叫⼀个线程停留在⼀个⽔壶那,直到这个⽔壶烧开,才去处理下⼀个⽔壶。但是实际上线程在等待⽔壶烧开的时间段什么都没有做。
同步和异步
同步与异步的区别同步:发送⼀个请求,等待返回,再发送下⼀个请求,同步可以避免出现死锁,脏读的发⽣。异步:发送⼀个请求,不等待返回,随时可以再发送下⼀个请求,可以提⾼效率,保证并发。
Socket和serversocket
socket
socket可以使一个应用从网络中读取和写入数据,不同计算机上的两个应用可以通过连接发送和接受字节流。当发送消息时,你需要知道对方的ip和端口。在java中,socket指的是java.net.Socket类。
一旦成功创建一个Socket类的实例,可以用它来发送和接收字节流,发送时调用getOutputStream方法获取一个java.io.OutputStream对象,接收远程对象发送来的信息可以调用getInputStream方法来返回一个java.io.InputStream对象。
serversocket
Socket类代表一个客户端套接字,即任何时候连接到一个远程服务器应用时构建所需的socket。
现在,要实现一个服务器应用,需要不同的做法。服务器需随时待命,因为不知道客户端什么时候会发来请求,此时,我们需要使用ServerSocket,对应的是java.net.ServerSocket类。
ServerSocket与Socket不同,ServerSocket是等待客户端的请求,一旦获得一个连接请求,就创建一个Socket示例来与客户端进行通信。
简易的socket与serversocket交互
实现功能:
1.接收用户消息,接收到之后返回相同内容。
步骤:
1.创建serversocket,设置监听端口
ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT);
2.接收客户端socket连接
Socket socket = serverSocket.accept();这里返回的是用户的socket,我们可以通过输入流往这里写入消息即可发送给用户
3.创建输入流和输出流,输入流;服务端一直等待用户的输入流,当接收到之后,使用输出流输入到socket中。
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
4.连续读取用户的输入,我们使用while语句连续读取用户的输入,注意readLine方法,遇到\n换行符停止,所以我们返回的消息也添加了\n换行符,客户端也可以使用readLine方法
while ((msg = reader.readLine())!=null){
//读取换行符之前的,注意此时msg可能为null,因为输入流可能会随时关闭
System.out.println("收到客户端:\n" + socket.getPort() +"的消息是" + msg);
// 回复客户发送的信息,注意这里添加了换行,所有客户端也可以使用readLine,比较方便
writer.write("服务器" + msg + "\n");
writer.flush();
if (QUIT.equals(msg)){
System.out.println("客户端" + socket.getPort() + "准备退出");
break;
}
}
}
5.退出功能,看上方if语句,当用户发送的消息与设定的quit字符串相等,即退出
6.关闭服务区
serverSocket.close();
下面是完整代码
public class Server {
public static void main(String[] args) {
final int DEFAULT_PORT = 8888;
final String QUIT = "quit";
ServerSocket serverSocket = null;
try {
//1.创建ServerSocket
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务器,监听端口" + DEFAULT_PORT);
while (true){
// 假如没收到请求,这里会阻塞,即等待客户端连接,这个方法返回一个Socket,用于往客户端发送消息
Socket socket = serverSocket.accept();
System.out.println("客户端" + socket.getPort() + "已经连接");
// 2.创建Writer和Reader,Writer往客户端写数据,Reader从客户端读数据
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String msg = null;
// 3.这里是循环读取用户发来的消息,要注意的是,这里readLine是读取到\n停止
while ((msg = reader.readLine())!=null){
//读取换行符之前的,注意此时msg可能为null,因为输入流可能会随时关闭
System.out.println("收到客户端:\n" + socket.getPort() +"的消息是" + msg);
// 回复客户发送的信息,注意这里添加了换行,所有客户端也可以使用readLine,比较方便
writer.write("服务器" + msg + "\n");
writer.flush();
// 4.如果退出命令和设置的一样,退出while循环
if (QUIT.equals(msg)){
System.out.println("客户端" + socket.getPort() + "准备退出");
break;
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (serverSocket != null){
try {
serverSocket.close();
System.out.println("关闭服务端");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
客户端实现功能:
1.等待用户输入
2.输入后,消息输入给服务端
3.接受服务端返回的消息
步骤:
1.指定目标服务区地址和接口
Socket socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
2.创建输入流输出流,以及等待用户输入的输入流
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 1.等待用户输入信息,一共有几种等待用户输入信息的方式,优点缺点
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
3.发送用户输入内容,接收服务器返回内容,注意这里的readLine,服务器返回的消息是增加了一个换行符,所以这里才可以使用readLine方法读取
while (true){
String input = consoleReader.readLine();
// 2.将输入信息发送
System.out.println("您可以输入内容");
bufferedWriter.write(input+"\n");
bufferedWriter.flush();
// 3.读取服务器返回消息
String msg = bufferedReader.readLine();
System.out.println("收到服务器返回消息" + msg);
if (QUIT_CLIENT.equals(input)){
System.out.println("客户端" + socket.getPort() + "准备退出");
break;
}
}
下面是完整代码
public class Client {
public static void main(String[] args) {
final String DEFAULT_SERVER_HOST = "127.0.0.1";
final String QUIT_CLIENT = "quit";
final int DEFAULT_SERVER_PORT = 8888;
Socket socket = null;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
// 创建socket
try {
// 在这一步,就已经连接上服务端了
socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
System.out.println("已经连接服务器");
// 读取返送的信息的
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 1.等待用户输入信息,一共有几种等待用户输入信息的方式,优点缺点
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
while (true){
String input = consoleReader.readLine();
// 2.将输入信息发送
System.out.println("您可以输入内容");
bufferedWriter.write(input+"\n");
bufferedWriter.flush();
// 3.读取服务器返回消息
String msg = bufferedReader.readLine();
System.out.println("收到服务器返回消息" + msg);
if (QUIT_CLIENT.equals(input)){
System.out.println("客户端" + socket.getPort() + "准备退出");
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if (bufferedWriter != null){
try {
bufferedWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
BIO同步阻塞模型
服务端需要对客户端的每个请求处理完成后 才会继续接受客户端的请求
客户端也会等待服务端处理完请求后才会发送请求
通常会使用多线程去处理 因为BIO每个连接一个单独的线程
思考上方代码
用户端在输入的时候,其实就是阻塞的,且因为是单线程,用户在阻塞的时候无法发送消息,无法接收消息
服 务端呢:在等待于客户端连接的过程中,也是阻塞的。
仅仅是上方的代码,我们可以实现服务器和客户端,1对1的进行信息交互,但是需要多个客户端相互通信,服务端就至少需要保存客户端的socket信息,不过读取用户数据是阻塞的,就需要为每个客户端创建一个新线程。而客户端在输入的时候,是阻塞的,但是我们希望在“阻塞”的时候也能接收显示其他用户发送的消息。
所以说客户端可以这样做:
主线程连接服务端,接收服务端发送的消息,创建一个子线程用于等待用户的输入
服务端可以这么做:
主线程接收用户请求,当有一个用户连接时,就创建一个线程,然后通过该线程和客户进行数据交互,同时使用集合保存用户的socket,当接收到一个用户的数据,遍历集合获取其他用户的socket,然后发送数据
实战多人聊天室
当一个用户连接时,主线程创建一个handler线程去控制和client的连接,当一个用户发送一个消息,handler要把该消息发给送给所有在线的client,所有要有一个管理client的数组
注意:客户端在等待用户输入的时候,也是阻塞的,此时我们也是希望用户能够同步显示其他client发送来的消息.所以说客户端也至少需要一个输入线程和一个接收其他用户消息线程.
功能划分:
ChatServer:作为服务端的主要类,我们可以在此类中进行一些基本处理操作,最主要的是该类中保存了所有在线用户的socket。
1.开启,在该方法中接收用户的连接,连接后就创建线程,在现场中接收用户的发送消息,然后在线程中调用ChatServer的转发方法
2.新用户到map中,因为转发功能需要把消息发送给除了该用户的其他用户,所以需要使用一个集合存储用户的socket
3.移除用户,当用户输入了退出指令,调用移除方法就可以移除用户
4.关闭流
public class ChatServer {
private int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 我们保存用户的端口号作为用户ID和对应的Writer,那么
private Map<Integer, Writer> connectedClients;
public ChatServer(){
connectedClients = new HashMap<>();
}
// 当一个用户连接上之后,加入到客户列表
// 这里注意,我们这里选择不解决异常,而是抛出异常,让调用方来决定,怎么处理这个异常
// 注意,这个方法是在多线程环境下被调用的,使用了synchronized
public synchronized void addClient(Socket socket) throws IOException {
if (socket != null){
int port = socket.getPort();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
connectedClients.put(port,bufferedWriter);
System.out.println("客户端[" + port + "]已经连接到服务器");
}
}
public synchronized void removeClient(Socket socket) throws IOException {
if (socket != null) {
int port = socket.getPort();
// 判断是否存在该用户
if (connectedClients.containsKey(port)) {
connectedClients.get(port).close();
}
System.out.println("客户端[" + port + "]已经断开连接");
}
}
// 把收到的一个消息,遍历map,然后发给除了自己的人
public synchronized void forwardMessage(Socket socket,String fwdMsg) throws IOException {
for (Integer id :connectedClients.keySet()){
if (!id.equals(socket.getPort())){
Writer writer = connectedClients.get(id);
writer.write(fwdMsg);
writer.flush();
}
}
}
// 判断用户发送的消息是否与退出命令一样
public boolean readyToQuit(String msg){
return msg.equals(QUIT);
}
// 启动服务器端,绑定监听端口
public void start(){
try {
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("服务器已经启动,正在监听 " + DEFAULT_PORT + "端口");
while (true){
// 等待客户端连接
Socket socket = serverSocket.accept();
// 连接后创建一个 ChatHandler线程,然后在该线程里面假如到map中,在那个线程中进行信息交换
new Thread(new ChatHandler(this,socket)).start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
}
}
// 这里会更新serverSocket的状态
public synchronized void close(){
if (serverSocket != null){
try {
serverSocket.close();
System.out.println("关闭serverSocket");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
ChatHandler:服务端用于于每个客户进行交互的线程,在该线程内接收用户信息,调用ChatServer方法转发信息等
public class ChatHandler implements Runnable{
private ChatServer chatServer;
private Socket socket;//客户端交互的socket
public ChatHandler(ChatServer chatServer,Socket socket){
this.chatServer = chatServer;
this.socket = socket;
}
@Override
public void run() {
try {
// 存储新上线的客户
chatServer.addClient(socket);
// 读取用户发送的消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = null;
while ((msg = bufferedReader.readLine()) != null){
//加了换行,客户端可以直接readLine
String fwdMsg = "客户[" + socket.getPort() + "]: " + msg + "\n";
System.out.println(fwdMsg);
// 转发给其他聊天室中在线的其他用户
// 因为我们希望两边都是用readLine来读取,其读取换行符之前的内容
chatServer.forwardMessage(socket,fwdMsg);
// 这里不要这么写!!可以单独写成一个方法,然后将QUIT设置成某个字符串,然后判断是否等于字符串
if (chatServer.readyToQuit(msg)){
chatServer.removeClient(socket);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally {
try {
//如果该线程处理的用户下线或者异常,这里移除该用户
chatServer.removeClient(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
ChatClient:客户端用于开启与服务器的连接,该类中有发送信息,接收信息方法,开启后首先连接服务器,然后该线程可以接收服务器传来的消息,还有一个是输入线程,因为输入可能会发送阻塞,所以使用了多线程。
public class ChatClient {
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader bufferedReader;
private BufferedWriter bufferedWriter;
public ChatClient(){
}
// 发送消息给服务器
public void send(String msg) throws IOException {
if (!socket.isOutputShutdown()){
bufferedWriter.write(msg + "\n");
bufferedWriter.flush();
}
}
public String receive() throws IOException {
String msg = null;
if (!socket.isInputShutdown()){
msg = bufferedReader.readLine();
}
return msg;
}
public boolean readyToQuit(String msg){
return QUIT.equals(msg);
}
public void start(){
// 创建socket对象
try {
socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_PORT);
System.out.println("连接成功");
// 创建对应的IO流
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 一方面接收用户输入,一方面展示转发信息
new Thread(new UserInputHandler(this)).start();
// 读取服务器转发信息
String msg = null;
while ((msg = receive()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally {
close();
}
}
public void close(){
if (bufferedWriter != null){
try {
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
System.out.println("退出");
}
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
client.start();
}
}
用户输入线程
public class UserInputHandler implements Runnable{
private ChatClient client;
public UserInputHandler(ChatClient client){
this.client = client;
}
@Override
public void run() {
// 等待用户输入消息
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("现在你可以在聊天室里面发送消息了");
while (true){
String input = bufferedReader.readLine();
client.send(input);
// 检查是否准备退出
if (client.readyToQuit(input)){
break;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
BIO模型的缺点
当用户多时,会创建大量的线程,大量线程会是否消耗资源
所以说BIO⽅式适⽤于连接数⽬⽐较⼩且固定的架构,这种⽅式对服务器资源要求⽐较⾼,