socket通信
一、socket通信基本原理
![img](https://i-blog.csdnimg.cn/blog_migrate/378054558489e7f3ce9925262ceede37.png)
socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,对用户来说,只要通过一组简单的API就可以实现网络的连接。
![img](https://i-blog.csdnimg.cn/blog_migrate/7f91fbbe6932730cb5fff25547aa5ab8.png)
服务端初始化ServerSocket,对指定的端口进行绑定,然后对端口及进行监听,通过调用accept方法阻塞端口。此时,客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。
二:socket通信基本示例:
服务端代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocket{
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取一行数据
String str = bufferedReader.readLine();
//输出打印
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码:
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost", 9999);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str = "你好,这是我的第一个socket";
bufferedWriter.write(str);
//刷新输入流
bufferedWriter.flush();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
问题1: 运行上面代码出现 java.net.SocketException: Connection reset 报错
原因分析:由于Client 端发送完消息后,并没有发送一个结束标识给Server端,所以 Server端 在调用 readLine() 方法时阻塞。
解决方法:调用 socket.close() 或者调用**socket.shutdownOutput()方法,但是二者并不同,下面进行二者的比较。socket.close() 是将socket关闭连接,如果有服务端给客户端反馈信息,此时客户端是收不到的。而socket.shutdownOutput() **是将输出流关闭,此时,如果服务端有反馈信息,则客户端是可以正常接收的。
修改代码如下:
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost", 9999);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str = "你好,这是我的第一个socket";
bufferedWriter.write(str);
//刷新输入流
bufferedWriter.flush();
//关闭输出流
socket.shutdownOutput();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三:while循环连续接受客户端信息:
Server端需要指定统一的编码格式如下:
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
通过while((str=bufferedReader.readLine()) != null) 判断输入流是否结束,此时服务端会一直阻塞,等待客户端输入。如下:
//通过while循环不断读取信息
while((str=bufferedReader.readLine()) != null) {
//输出打印
System.out.println(str);
}
服务端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest1 {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息
while((str=bufferedReader.readLine()) != null) {
//输出打印
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientSocket1 {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket = new Socket("localhost", 9999);
//通过socket获取字符流
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while(true) {
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
四:多线程下socket编程
由于socket通信是阻塞式的,假设我现在有A和B俩个客户端同时连接到服务端的上,当客户端A发送信息给服务端后,那么服务端将一直阻塞在A的客户端上,不同的通过while循环从A客户端读取信息,此时如果B给服务端发送信息时,将进入阻塞队列,直到A客户端发送完毕,并且退出后,B才可以和服务端进行通信。简单地说,我们现在实现的功能,虽然可以让客户端不间断的和服务端进行通信,与其说是一对一的功能,因为只有当客户端A关闭后,客户端B才可以真正和服务端进行通信,这显然不是我们想要的。(参考)
服务端:
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientSocket2 {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket = new Socket("localhost", 9999);
//通过socket获取字符流
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while(true) {
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest2 {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
while(true) {
//等待客户端的连接
final Socket socket = serverSocket.accept();
//每当有一个客户端连接进来后,就启动一个单独的线程进行处理
new Thread(new Runnable(){
@Override
public void run() {
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine())!=null){
//输出打印
System.out.println("客户端说:"+str);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端A 发送消息给服务端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1fWnVJaK-1584793377409)(C:\Users\yc\AppData\Roaming\Typora\typora-user-images\image-20200321113059105.png)]
客户端B发送消息给服务端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkEoFEOl-1584793377410)(C:\Users\yc\AppData\Roaming\Typora\typora-user-images\image-20200321113121396.png)]
服务端能同时接收到两个客户端的消息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sKUGwtxC-1584793377410)(C:\Users\yc\AppData\Roaming\Typora\typora-user-images\image-20200321113158095.png)]
但是以上通过 new Thread(new Runnable()方法 产生新的线程,只有在客户端比较少的时候适用,一旦大批量的客户端连接,会出现大批量创建线程后的,在发送消息后,大量无用线程的占用。所以,通过线程池技术,保证线程复用 。修改后的服务端如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
while(true) {
//等待客户端的连接
final Socket socket = serverSocket.accept();
//每当有一个客户端连接进来后,就启动一个单独的线程进行处理
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine())!=null){
//输出打印
System.out.println("客户端说:"+str);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
executorService.execute(thread);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
}
}
});
executorService.execute(thread);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
引用如下:
[1]: https://www.jianshu.com/p/cde27461c226 "Java socket详解,看这一篇就够了"