文章目录
一. 通过java用TCP实现网络编程
api介绍
1. ServerSocket
ServerSocket是专门给服务器用的api
构造方法:
方法:
2. Socket
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据
的
构造方法:
方法:
代码实现
服务器:
第一步: 创建对象
第二步: 实现start
2.1 首先要建立连接
这个ServerSocket的作用, 其实就是为了连接, 连接完成之后, 返回的是Socket对象, 接下来服务器进行的工作都是Socket完成的
调用start方法后, 如果没有客户端发送请求, 那么就是在accept这里阻塞等待
单独整理一个方法处理连接后的逻辑, 需要循环处理客户端的请求
第三步: 实现processConnection方法
3.1 打印一个日志, 告知服务器当前有客户端连上了
3.2 从socket获取流对象, 来进一步进行后续操作
因为TCP是字节流传输, 所以可以使用InputStream, OutputStream来接收客户端的数据, 从而进行读写操作
3.3 读取请求并解析
- 为什么不适用read , 而是使用scanner
使用read返回的是字节数组, 那么为了后续方便打印, 还需要将字节数组转成String
而InputStream本身就可以搭配Scanner使用, 此时scanner.next返回的直接是String - if条件判断的含义
如果客户端终止了, 那么scanner.hasNext返回的就是false, 取反就是true, 此时就表示客户端已经断开连接了, 就可以直接break, 无需执行后面的逻辑了
如果客户端没有终止, 但是没有发送数据过来, 此时hasNext是阻塞的
如果发送了数据过来, 那么hasNext返回true, 取反false, 不会进入if中, 就会继续执行后面的逻辑 - 但是使用scanner有个弊端, scanner.next这个读取方式, 只有读到"空白符"才会读取完毕, 不然就会一直阻塞, 直到有"空白符"请求为止, 所以就要求客户端在发送数据的时候, 务必要在每个请求的末尾加上空白符
空白符是一类字符的通称, 包括 换行, 回车, 空格, 制表符, 翻页符...
3.4 根据请求计算响应
由于是回显程序, 直接返回即可
但是要明确给数据加上一个"空白符", 防止阻塞
3.5 把响应写回给客户端
3.6 打印日志
第四步: 实现main方法
完整代码:
java
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while(true){
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
//1. 打印一个日志, 告知说当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
//2. 从socket获取流对象, 来进一步进行后续操作
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//3. 读取请求并响应
Scanner scanner = new Scanner(inputStream);
while(true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.next();
//4. 根据请求计算响应
String response = process(request);
//5. 把响应写回到客户端
outputStream.write(response.getBytes(), 0, response.getBytes().length);
//6. 服务器打印日志
System.out.printf("[%s:%d] res=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request,response);
}
}
}
private String process(String request) {
return request + "\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端:
第一步: 创建对象
这样的构造方式, 就完成了和服务器之间的连接
第二步: 完成start
2.1 准备工作
打印日志
从socket获取字节流对象, 为后续接收发送数据做准备
用scannerNetwork来接收服务器返回的结果, 不用再进行转字符操作
2.2从控制台读取数据
2.3 把请求发送给服务器
因为服务器那边只有接收到"\n"才会停止读取, 所以我们手动加上
使用outputStream.write来发送数据
2.4 从服务器读取响应
如果服务器断开或者没有连接上服务器, scannerNetwork返回false, 取反true, 就会break
如果连接上了服务器, 就会返回, 用response接收
2.5 把响应显示到控制台上
第三步: 完成main方法
完整代码:
java
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerNetwork = new Scanner(inputStream);
while(true){
//1. 从控制台读取数据
System.out.println("请输入要发送的数据:");
String request = scanner.next();
//2. 把请求发送给服务器
request += "\n";
outputStream.write(request.getBytes());
//3. 从服务器读取响应
if(!scannerNetwork.hasNext()){
break;
}
String response = scannerNetwork.next();
//4. 把响应显示到控制台上
System.out.println(response);
}
}catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
运行结果:
上述代码存在的问题
1. 服务器中, 对于accept创建的socket对象, 没有进行关闭操作
服务器serverSocket是不必关闭的, 因为他的声明周期是跟随整个服务器进程的, 他要一直等待连接
客户端的socket, 也是不必关闭的, 它跟随客户端的生命周期, 客户端结束它才要结束
但是服务器的clientSocket就不可以不关闭了, 因为每个客户端都有对应的clientSocket, 如果用完了不关闭, 就会使当前的clientSocket对应的文件描述附表得不到释放, 引进文件资源泄露
解决办法:
我们可以在processConnection中加入finally或者将clientSocket方法try()中
**2. 当前这个代码, 服务器是无法同时给多个客户端提供服务的
启动多个客户端, 服务器是感知不到的, 只能当上一个客户端终止, 下一个客户端才能连接上
原因:
我们这个代码, 当一个客户端正在连接时, 此时进入到processConnection方法中, 进行while循环, 如果第二个客户端来了, 是没法执行到accept的
解决办法:
可以使用多线程, 让连接客户端和处理客户端的响应可以一起进行
注意: 此时就只能在processConnection中close掉clientSocket
服务器完整代码:
java
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while(true){
Socket clientSocket = serverSocket.accept();
Thread thread = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
private void processConnection(Socket clientSocket) throws IOException {
//1. 打印一个日志, 告知说当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
//2. 从socket获取流对象, 来进一步进行后续操作
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//3. 读取请求并响应
Scanner scanner = new Scanner(inputStream);
while(true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.next();
//4. 根据请求计算响应
String response = process(request);
//5. 把响应写回到客户端
outputStream.write(response.getBytes(), 0, response.getBytes().length);
//6. 服务器打印日志
System.out.printf("[%s:%d] res=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request,response);
}
}finally{
clientSocket.close();
}
}
private String process(String request) {
return request + "\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}