✨哈喽,进来的小伙伴们,你们好耶!✨
🛰️🛰️系列专栏:【JavaEE】
✈️✈️本篇内容:基于TCP的客户端服务器程序。
🚀🚀代码存放仓库gitee:JavaEE初阶代码存放!
⛵⛵作者简介:一名双非本科大三在读的科班Java编程小白,道阻且长,星夜启程!
接着上篇博客,我们继续来学习网络编程套接字socket的相关知识点,上篇博客写了一个最简单的UDP版本的回显服务,那么这里我们来写一个稍微带点业务逻辑的翻译程序(英译汉)。
一、简单翻译程序
即客户端不变,把服务代码进行调整,关键的逻辑就是把响应写回给客户端。
package NetWork;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
/**
* 一个翻译功能的服务器客户端程序
*/
public class UdpDictServer extends UdpEchoServer {
private HashMap<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//简单构造几个词
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("pig","猪猪");
dict.put("duck","鸭");
}
public String process(String requset){
return dict.getOrDefault(requset,"该词无法被翻译!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
OK,那么我们首先启动服务器。
然后启动我们的客户端,输入相应的英文单词,观察服务器的响应。
OK,客户端这边没有问题,我们在点击到服务器这里观察一下。
OK,那么上一篇博客加上面的内容就是UDP版本的客户端服务器代码的全部内容了,今天我们来学习一下TCP版本的客户端服务器代码。
一、TCP API
那么TCP API中,也是涉及到两个核心的类,SeverSocket(专门给tcp服务器用的) ;Socket(急即要给服务器用,又需要给客户端用)。
老样子,我们先写tcp版本的 TcpEchoServer 的代码:
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);//绑定端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
Socket clinetSocket = serverSocket.accept();
processConnection(clinetSocket);
}
}
前面基本都和UDP的差不多,区别就是这里的start方法,由于tcp是有连接的,不是一上来就能读数据,需要先建立连接(打电话);accept返回了一个socket对象,accept可以理解为接电话,那么接电话的前提就是有人给你打电话,如果无,那么这里accept就会阻塞。
OK,接下来我们来写processConnection()方法的代码。
这里也分为三步:
step1: 循环处理每个请求,分别返回响应。
step2: 根据请求,计算响应。
step3:把这个响应返回给客户端。
private void processConnection(Socket clinetSocket) {
System.out.printf("[%s:%d客户端建立连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
//接下来处理请求与响应
try(InputStream inputStream = clinetSocket.getInputStream()){
try(OutputStream outputStream = clinetSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//读取请求
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
break;
}
String request = scanner.next();
String response = process(request);
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果。
printWriter.flush();
System.out.printf("[%s:%d] rep:%s,resp:%s\n",
clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//记得关闭操作
try {
clinetSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
注意上述代码特意针对这里的clientSocket关闭了一下,但是却没有对SeverSocket关闭,那么关闭是在干什么?释放资源!释放资源的前提是这个资源不再使用了,对于UDP的程序serversocket来说,这些socket都是贯穿始终的,最迟也是随着进程一起退出,但是对于TCP来说,这个是每个连接的一个,有很多,断开也就不在需要了,每次都得保证处理完的连接都进行释放。
TcpEchoServer完整代码:
package NetWork;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);//绑定端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//由于tcp是有连接的,不是一上来就能读数据,需要先建立连接(打电话)
//accept返回了一个socket对象
Socket clinetSocket = serverSocket.accept();
processConnection(clinetSocket);
}
}
private void processConnection(Socket clinetSocket) {
System.out.printf("[%s:%d客户端建立连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
//接下来处理请求与响应
try(InputStream inputStream = clinetSocket.getInputStream()){
try(OutputStream outputStream = clinetSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//读取请求
while (true){
//循环的处理每个请求 分别返回响应
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
break;
}
String request = scanner.next();
//2、根据请求,计算响应!
String response = process(request);
//3、把这个响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果。
printWriter.flush();
System.out.printf("[%s:%d] rep:%s,resp:%s\n",
clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//记得关闭操作
try {
clinetSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
OK,接下来我们写 TcpEchoClinet 的代码。
public class TcpEchoClinet {
private Socket socket = null;
public TcpEchoClinet(String serverIp,int serverport) throws IOException {
socket = new Socket(serverIp,serverport);
}
注意,这里传入的IP和端口号的含义表示不是自己绑定,而是表示和这个ip端口号建立连接。
start()方法:
public void start(){
System.out.println("和服务器连接成功!");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream()){
try(OutputStream outputStream = socket.getOutputStream()){
while (true){
//要做的事情,仍然是四个步骤
//1.从控制台读取字符串
System.out.println("->");
String request = scanner.next();
//2、根据读取的字符串,构造请求,发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3、从服务器读取响应,解析
Scanner respscanner = new Scanner(inputStream);
String response = respscanner.next();
//4、把结果显示到控制台上。
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
main函数
public static void main(String[] args) throws IOException {
TcpEchoClinet clinet = new TcpEchoClinet("127.0.0.1",9090);
clinet.start();
}
OK,老规矩我们先启动服务器,再启动客户端。
服务器启动成功,我们启动客户端,可以发现和服务器连接成功!
然后我们点到服务器这边观察一下,可以发现服务器已经和客户端建立连接,系统已经分配了端口号。
OK,我们输入数据测试一下。
看服务器这边的响应,没有问题。
那么我们结束客户端,看服务器这边的响应。
虽然上述代码已经运行起来了,但是存在一个问题,就是当前的服务器同一时间只能处理一个连接,我们看如何验证?
我们再次启动一个客户端,看服务器这边有没有响应。
我们发现服务器这边没有再次出现客户端建立连接的结果,只有第一次的客户端程序可以得到响应。
那么这是为什么呢?
我们可以发现这里的代码第一次accept结束之后,就会进入processConnection,在processConnection中又有一个循环,若processConnection里面的循环不停,processConnection就无法完成,就会导致外层循环无法进入下一轮,也就无法第二次调用accept了。
那么如何解决思路是什么呢?这里就得让processConnection的执行和前面的accept的执行互相不干扰。这里就得用到咋们之前学的多线程的知识啦!
那么之前为什么UDP版本的程序就不需要多线程就可以处理多个请求呢?
因为UDP不需要连接,只需要一个循环就可以处理所有客户端的请求,但是TCP即需要处理连接,又需要处理一个连接中的多个请求。
解决方案:
让主线程循环调用accept,当有客户端连接上来的时候就让主线程创建一个新线程,由新线程负责客户端的若干个请求,这个时候多个线程看上去是同时执行的。
这里我们新写一个类TcpThreadEchoServer,在原有的TcpEchoServer基础上修改以下部分代码即可。
Thread t = new Thread(()->{
processConnection(clinetSocket);
});
t.start();
TcpThreadEchoServer代码:
package NetWork;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpThreadEchoServer {
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
Socket clinetSocket = serverSocket.accept();
Thread t = new Thread(()->{
processConnection(clinetSocket);
});
t.start();
}
}
private void processConnection(Socket clinetSocket) {
System.out.printf("[%s:%d客户端建立连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
try(InputStream inputStream = clinetSocket.getInputStream()){
try(OutputStream outputStream = clinetSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//读取请求
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
break;
}
String request = scanner.next();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果。
printWriter.flush();
System.out.printf("[%s:%d] rep:%s,resp:%s\n",
clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//记得关闭操作
try {
clinetSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
OK,我们再次启动多线程版本的服务器代码,然后启动两个客户端,发现没有问题。
OK,那么到这里我们的网络编程socket就已经全部学习完毕了,下一节博主将会持续更新TCP/IP五层协议栈的详解,感谢小伙伴的一键三连支持!!