JavaEE-网络编程(2)
目录
1. TCP的socket api
1.1 ServerSocket
1.2 Socket
1.3 关于连接
2. 写一个TCP回显服务器
代码的基本结构
2.1.建立连接
2.2 使用 try catch 语法
2.3 对操作流进行封装
2.4 使用 flush() 冲刷缓冲区
2.5 用 close() 关闭对客户端的连接
2.6 println 和 hasnext
3. TCP 客户端
4.一个服务器同时给多个客户端服务
4.1 现象以及原因分析
4.2 优化方案:创建多线程
4.3 利用线程池进一步优化
1. TCP的socket api
1.1 ServerSocket
ServerSocket 是专门给服务器用的
构造方法如下:
与UDP类似,服务器启动,需要先绑定端口号
建立连接的方法如下:
与 UDP 不同,UDP 的特性是“无连接”,TCP 是“有连接”
这里的 accept 是联通连接的关键操作,返回值是一个 Socket 类型,需要用 Socket 类来接收
1.2 Socket
Socket 是服务器和客户端都会用的
构造方法:
用于接收请求和发送响应的方法:
TCP并没有 send / receive 这样的操作,使用的是字节流来进行接收和响应
TCP 的一个核心特点就是,面向字节流
读写数据的基本单位就是字节 byte
(UDP的单位是一个数据报)
关闭对客户端的连接:
1.3 关于连接
连接可以理解为打电话,客户端给服务器打电话,当电话打通了,就是建立了连接。
一旦打通了电话,就可以说话,说话可以说一句就挂断,也可以说很多句再挂断。
对于 TCP 服务器也是如此,一旦进行连接,就可以发送一个或者多个请求,直到想结束了,再断开连接。
2. 写一个TCP回显服务器
代码的基本结构
直接看代码:
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
//服务器连接客户端
public void start() throws IOException {
System.out.println("服务器启动!");
//不断的接收客户端的连接请求
while(true){
//与客户端进行连接
//如果没有客户端发送请求,accept会进入阻塞状态
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
//连接成功之后为客户端进行服务
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端已上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();//结束之后会自动关闭输入流
OutputStream outputStream=clientSocket.getOutputStream()//结束之后会自动关闭输出流
){
//针对 InputerStream 套了一层
Scanner scanner=new Scanner(inputStream);
//针对 OutputStream 套了一层
PrintWriter writer=new PrintWriter(outputStream);
//三个步骤
while(true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端已下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//1.接收请求
//byte[] request=new byte[1024];
//inputStream.read(request);
String request=scanner.next();
//2.根据请求计算响应
String response=process(request);
//3.发送响应
writer.println(response);
writer.flush();
//4.打印日志
System.out.printf("[%s:%d] req:%s resp:%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
}catch (IOException e){
throw new RuntimeException();
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
【注意】
2.1.建立连接
建立连接的时候要使用serverSocket.accept(),该方法的返回值是一个Socket类型,当成功建立连接后,再用 Socket 类的方法来操作字节流,进行接收请求和发送响应
2.2 使用 try catch 语法
操作打开后需要进行close操作,此处可以使用 try catch 语法将其包裹,在try()内打开,当try内的代码块执行完毕后就会自动关闭
2.3 对操作流进行封装
可以直接使用 inputStream.read 来读取请求的数据
也可以使用Scanner对inputStream进行封装
实际上,Scanner 的构造方法,填入的其实是一个 InputStream 对象:
我们平时使用的 System.in 其实就是一个 InputStream 类型:
也就是说,当 Scanner 的构造方法中,传入的是 System.in 的输入流时,就进行系统输入
当传入的是 clientSocket.getInputStream 的输入流时,就进行网络输入。
同理,outputStream 也可以使用 PrintWriter 进行封装:
2.4 使用 flush() 冲刷缓冲区
writer.println(request) 这个操作只是把数据放到“发送缓冲区”(内存空间)中,还没有真正写入到网卡里
要调用 flush()方法来“冲刷缓冲区”,才能真正发送数据
如果没有冲刷,是发送不出去的:
2.5 用 close() 关闭对客户端的连接
我在上篇文章说过:是否要对一个程序进行 close 操作,这取决于这个程序的生命周期
而此处 clientSocket 的生命周期就是从连接成功到断开连接,即使客户端与服务器断开了连接,服务器也依旧在运转,等待连接下一个客户端,也就是说,clientSocket 并没有伴随整个进程的始终
因此, clientSocket 是需要进行 close() 操作的
此处的 close() 操作应当放入 finally{} 代码块中,这样一来,无论程序如何执行,close() 操作都一定会被执行到。
2.6 println 和 hasnext
println 后面这个ln,行为是给字符串末尾加上\n
当把 ln 去掉,把 println 改成 print ,会发现服务器无法返回响应:
事实上是,客户端确实成功向服务器发送了数据,服务器也收到了数据,但是服务器却没有处理。
原因:
hasNext() 的行为是判定收到的数据中是否包含“空白符”(换行,回车,空格,制表符,翻页符...)
当遇到空白符,才会认为是一个“完整的next”,在遇到之前,都会阻塞
UDP 是以 DatagramPacket 作为单位的。
TCP 则是以 字节 为单位
但是实际上,一个请求往往是由多个字节构成的
此时就需要一个标记来区分,到底多少字节为一个“完整的请求”
于是就暗暗约定,一个请求/响应 使用 \n 作为结束标记
对端读的时候,也是读到 \n 就结束(认为读到了一个“完整的请求”)
3. TCP 客户端
public class TcpEchoClient {
Socket socket=null;
public TcpEchoClient(String serverIp,int port) throws IOException {
this.socket = new Socket(serverIp,port);
}
public void start(){
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()) {
//针对 InputerStream 套了一层
Scanner scannerNet=new Scanner(inputStream);
//针对 OutputStream 套了一层
PrintWriter writer=new PrintWriter(outputStream);
while(true){
//1.向服务器发送请求
System.out.println("请输入要发送的内容:");
String request=scanner.next();
writer.println(request);
writer.flush();
//2.接收服务器的响应
String response=scannerNet.next();
//3.打印响应
System.out.println(response);
}
}catch (IOException e){
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
4.一个服务器同时给多个客户端服务
4.1 现象以及原因分析
运行服务器,再运行客户端,客户端可以正常与服务器进行通信:
Tips:如何运行多个main?
如果在上述的基础上,再运行一个客户端,会发现第二个客户端无法正常通信:
原因:
服务器必须与客户端连接上了,才能够给客户端提供服务
然而此处服务器代码运行到 processConnection(clientSocket) 这一行时就没再往下执行了
因为当前线程还在为第一个客户端提供服务,所以后续客户端想要与服务器创建连接,必须阻塞等待
4.2 优化方案:创建多线程
当一个客户端与服务器成功连接时,创建一个新的线程,让这个新的线程单独服务这个客户端
此时的main线程就不需要进入 processConnection 方法内部,而是直接进入下一轮while循环
当代码运行到 serverSocket.accept() 时,如果有新的客户端请求连接,就创建连接
如果没有新的客户端请求连接,就阻塞等待,直到有新的客户端请求与服务器连接
public void start() throws IOException {
System.out.println("服务器启动!");
//不断的接收客户端的连接请求
while(true){
//与客户端进行连接
//如果没有客户端发送请求,accept会进入阻塞状态
Socket clientSocket = serverSocket.accept();
//创建新的线程,让新的线程来为客户端提供服务,使得当前main线程能够空闲下来,为客户端和服务器创建连接
Thread thread=new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
再次启动服务器和两个客户端:
服务器可以同时为两个客户端提供服务
4.3 利用线程池进一步优化
当前这个程序,有客户端连接上,就创建一个新的线程。客户端断开,就销毁一个线程。
而创建和销毁线程实际上是很消耗资源的操作。
因此可以引用线程池来解决这个问题:
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executorService= Executors.newCachedThreadPool();
//不断的接收客户端的连接请求
while(true){
//与客户端进行连接
//如果没有客户端发送请求,accept会进入阻塞状态
Socket clientSocket = serverSocket.accept();
//使用线程池
executorService.submit(()->{
processConnection(clientSocket);
});
}
}
完
如果哪里有疑问的话欢迎来评论区指出和讨论,如果觉得文章有价值的话就请给我点个关注还有免费的收藏和赞吧,谢谢大家!