- 博主简介:想进大厂的打工人
- 博主主页:@xyk:
- 所属专栏: JavaEE初阶
上一篇我们讲解了UDP回写服务器和简易翻译服务器,想了解本篇文章,建议先看看上篇文章,学起来会更容易一些~~传送门:(1条消息) 【JavaEE】UDP数据报套接字—实现回显服务器(网络编程)_xyk:的博客-CSDN博客
那么本篇文章我们来讲讲TCP回写服务器和简易翻译服务器~~~
目录
文章目录
一、小技巧
二、TCP网络编程
2.1 ServerSocket API
2.2 Socket API
2.3 长连接和短连接
三、用TCP编写一个客户端服务器程序
3.1 ServerSocket对象创建与构造方法
3.2 start启动方法
3.3 连接成功后的细节处理processConnect
四、TCP客户端编写
4.1 指定服务器Socket对象与构造方法
4.2 start启动方法
4.3 多个客户端运行bug
4.4 测试多个客户端
五、TCP简易翻译服务器
5.1 测试
一、小技巧
先讲个方便计算的小技巧:
- Thousand ==> 1KB(千)
- Million ==> 1MB(百万)
- Billion ==> 1GB(十亿)
对于简易翻译的服务器,要存储成千上万个单词,那么假设一个单词的内存占用为1kb,那么100w个单词 约等于10亿个字节,也就是1GB~~
二、TCP网络编程
TCP也是两个核心的类
ServerSocket:是给服务器用的~
Socket:既会给客户端使用,也会给服务器使用~
2.1 ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
是给服务器端指定一个端口号并绑定~~这个 Socket 和 DatagramSocket定位类似,都是在构造的时候指定一个具体的端口,让服务器绑定该端口
ServerSocket 方法:
方法签 名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
accept 意思就是接受~~
服务器是被动的一方
客户端是主动的一方
客户端主动向服务器发送连接请求,服务器就要同意一下~~
其实tcp连接的接受是在内核里已经完成了,但是实际上这个 accept 是应用层序层面的接受~~
socket对象如果周期很长,一般是不需要关闭的,因为关闭时一般意味着程序终结,资源自动回收
2.2 Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
表示服务器的 ip 和 端口号
TCP是有连接的,在客户端 new Socket 对象的时候,就会尝试和指定 ip 和 端口 的目标建立连接了
Socket 方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
注意这里是字节流!!TCP是面向字节流的!!
从inputStream 这里读数据,就相当于从网卡接受~
从OutputStream 这里写数据,就相当于从网卡发送~
2.3 长连接和短连接
服务器收到一个请求,就返回一个响应,然后断开连接
这就是短连接
服务器收到一个客户端的多条请求,再一起返回响应,然后断开连接
这就是长连接
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于
客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
对于接下来的代码,考虑到多条请求,我做出如下规定:
- 多条请求之间以空白符分割~
- 每一个请求都是字符串
对于后面的代码设计,是按长连接来设计的!
三、用TCP编写一个客户端服务器程序
首先还是创建一个客户端,一个服务器
先来编写服务器端
3.1 ServerSocket对象创建与构造方法
这个对象是专门给服务器用的,通过这个对象来获得客户端的发来的连接,可以把他当作售楼处的小哥(专门拉客)~~~
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
3.2 start启动方法
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
Socket clientSocket = serverSocket.accept();//建立连接
processConnect(clientSocket); // 连接成功后进行一些操作~
}
}
用之前的serverSocket(小哥)来accept连接,此时得到一个新的clientSocket,可以把这个当作售楼处的小姐姐(专业顾问)~~~
注意!!!在这里,每个客户都需要有一个专门的小姐姐来提供服务
此时,会有个疑问:
为什么要返回一个客户端的Socket对象??用之前的那个serverSocket不行吗??
因为服务器的输出流数据来自客户端的输入流,为了获取到数据,所以使用客户端的Socket来接收
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
3.3 连接成功后的细节处理processConnect
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
//1.读取请求
if (!scanner.hasNext()){
// 读取的流到了结尾了(对端关闭了)
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 直接使用 scanner 读取一段字符串.
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行的.
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
}
1. 客户端上线打印日志
2. 输入输出流处理
try () 这种写法,( ) 中允许写多个流对象,使用 ;来分割
括号内部为“打开文件流”操作,默认加了finally关闭该文件,让程序猿更加方便~~
3. 字节流转换字符流
通过这两个对象,实现传输过程
Scanner作为输入流,数据来源是客户端输入
PrintWriter作为输出流,数据去向是客户端的socket文件
没有这个 scanner 和 printWriter, 完全可以!!但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
- 多条请求之间以空白符分割~
- 每一个请求都是字符串
为了简单编码,此处进行字符流转化
4. 建立长连接
hasNext 判定接下来还有没有数据了
如果对端关闭连接,此时hasNext返回false,循环就让它结束
如果对端有数据,就用next方法来读取字符串内容
5. 计算响应并返回
注意这里的flush()方法,printWrier是将数据写入缓冲区(buffer),而不是写入网卡,此处需要我们进行手动刷新数据,将数据立即被写入网卡,这样才能让客户端读到响应!!!
写网卡操作称作IO操作,为了提高 IO 效率,引入缓冲区,使用缓冲区减少 IO 次数,就可以提高整体的效率~~
假设要写十次网卡,就先要把写的数据放到一个内存构成的缓冲区(buffer)中,再统一把这个缓冲区中的数据写入网卡~~
还需要注意写回的时候,要换行写回!!!
6. 关闭文件
clientSocket只是给一个连接提供服务的,这个东西还是要能够进行关闭的~~
四、TCP客户端编写
4.1 指定服务器Socket对象与构造方法
会用这个对象就行了,至于怎么通过端口获得对应的输入输出流的不重要
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int port) throws IOException {
// 这个操作相当于让客户端和服务器建立 tcp 连接.
// 这里的连接连上了, 服务器的 accept 就会返回.
socket = new Socket(serverIp,port);
}
4.2 start启动方法
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromServer = new Scanner(inputStream);
while(true) {
System.out.print("-> ");
String request = scanner.nextLine();
printWriter.println(request);
printWriter.flush();
String response = scannerFromServer.next();//如果为空,则需要等待对方flush
System.out.printf("[%s : %d] 收到请求:%s, 返回响应:%s\n", socket.getInetAddress().toString(),
socket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
1. 获得服务器的输入输出流
同样的方法
2. 字节流转换字符流
3. 请求与响应
注意,这里写的时候要加上换行,因为之前我们有规定
也是需要刷新一下缓冲区,让其立即写入网卡!!!
4.3 多个客户端运行bug
此时如果我们运行多个客户端,上述的代码显然是不可以的,我们可以测试一下
我们启动两个客户端
第一个客户端输入请求:
第二个客户端输入请求:
此时可以发现第二个客户端的数据请求,服务器没有响应!!!
那么为什么会这样?
因为我们的服务器只accept了一次,只有一个小姐姐在提供服务
processConnect还没有执行完,没有退出
解决方法:
我们可以使用多线程或者线程池来解决,只要让服务器并发的去执行任务就ok了!
1.多线程写法
while (true){
Socket clientSocket = serverSocket.accept();
// 如果直接调用, 该方法会影响这个循环的二次执行, 导致 accept 不及时了.
// 创建新的线程, 用新线程来调用 processConnection
// 每次来一个新的客户端都搞一个新的线程即可!!
Thread t = new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
2.线程池写法
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
主线程只做两件事:
- 1.accept
- 2.创建线程
当线程创建好了,就会立即下一次调用accept,与此同时,刚创建出来的新线程,去循环处理客户端请求
4.4 测试多个客户端
此时,就解决了多个客户端同时在线~~~
五、TCP简易翻译服务器
package network;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author xyk的电脑
* @version 1.0
* @description: TODO
* @date 2023/4/17 17:26
*/
public class TcpDictServer extends TcpEchoServer{
private Map<String,String> map = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
map.put("cat","小猫");
map.put("dog","小狗");
map.put("pig","小猪");
}
@Override
public String process(String request){
return map.getOrDefault(request,"查无此词!");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}