这一节的内容首先是对十五弹(UDP回显服务器)进行简单的改进,在这基础上开始介绍TCP流套接字编程。
目录
上一节中的回显服务器,缺少业务逻辑,这里我们对代码进行改进,实现一个查词典的功能。
这里需要的是需要重写一下process方法!
- package network;
-
- import java.io.IOException;
- import java.net.SocketException;
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * Created with IntelliJ IDEA.
- * Description:
- * User: 苏西西
- * Date: 2023-10-19
- * Time: 21:55
- */
- //对于DictServer来说,和EchoServer相比大部分东西都是一样的,继承复用原来的代码
- //主要是根据请求计算响应这个步骤不太一样
- public class UdpDictServer extends UdpEchoServer{
- private Map
dict = new HashMap<>(); - public UdpDictServer(int port) throws SocketException {
- super(port);
- //给这个dict设置一下内容
- dict.put("cat","小猫");
- dict.put("dog","小狗");
- dict.put("pig","zyt");
- //可以无限多设置
-
- }
- @Override
- public String process(String request){
- //查词典
- return dict.getOrDefault(request,"查不到!");
-
- }
-
- public static void main(String[] args) throws IOException {
- UdpDictServer server = new UdpDictServer(4000);
- server.start();
-
- }
- }
TCP并不需要一个类来表示“TCP”数据报。
TCP不是以数据报为单位进行传输的,是以字节的方式,流式传输的。
ServerSocket 专门给服务器使用的Socket对象。
方法签名 | 方法说明 |
ServerSocket(int port) 这里的port指的是服务器要绑定的端口。 | 创建一个服务端流套接字Socket,并绑定到指定端口。 |
方法签名 | 方法说明 |
Socket accept( ) | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭套接字 |
这里,accept() 类比于接电话;返回一个服务端Socket对象:接电话后会返回一个Socket对象,通过这个socket对象和客户端进行沟通。
Socket 既会给客户端使用,也会给服务器使用。
在服务器这边,是由accept返回的;在客户端这边,代码构造的时候指定一个IP和端口号(此处值指的是服务器的IP和端口),有了这个信息就可以和服务器建立连接了。
方法签名 | 方法说明 |
Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。 |
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
进一步通过Socket对象,获取到内部的流对象,借助流对象来进行发送/接收。
- 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){
- //使用这个clientSocket和具体的客户端进行交流。
- Socket clientSocket = serverSocket.accept();
- processConnection(clientSocket);
- }
- }
-
- //使用这个放方法来处理一个连接。
- //这一个连接对应到一个客户端,但是这里可能会涉及到多次交互。
- private void processConnection(Socket clientSocket) {
- System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
- //基于上述socket对象和客户端进行通信
- try(InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()) {
- //由于要处理多个请求和响应,也是使用循环来进行的
- while (true){
- //1.读取请求
- Scanner scanner = new Scanner(inputStream);
- if (!scanner.hasNext()){
- //如果没有下一个说明读完了,客户端关闭了连接
- System.out.printf("[%s:%d] 客户端下线\n!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
- break;
- }
- //注意:此处使用的next是一直读取到换行符/空格/其他空白符结束
- //最终返回结果里不包含上述空白符
- String request = scanner.next();
-
- //2.根据请求构造响应
- String response = process(request);
-
- //3.返回响应结果
- //对于OutputStream 没有write String这个功能,可以把String里的字节数组拿出来,进行写入;
- //也可以用字符流来转换一下
- PrintWriter printWriter = new PrintWriter(outputStream);
- //此处使用println来写入,让结果中带有一个\n换行,方便对端(接收端)接收解析
- printWriter.println(response);
- //flush用来冲刷缓冲区,保证当前写入的数据确实发送出去了
- printWriter.flush();
-
- //打印一个日志
- System.out.printf("[%s:%d] req:%s;resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
- request,response);
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- //更合适的做法是将close放到finally里面,保证close()一定会被执行到
- try {
- clientSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- public String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer server = new TcpEchoServer(1111);
- server.start();
- }
-
- }
这里的accept() 效果是“接收连接”,前提是得有客户端来建立连接。客户端在构造Socket对象的时候就会指定服务器的IP和端口,如果没有客户端来连接,此时的accept就会阻塞。
TCP socket里面涉及两种socket对象。
这个代码中,用到了一个clientSocket,此时任意一个客户端连上来都会返回/创建一个Socket对象。(Socket就是文件)每次创建一个clientSocket对象就要占用一个文件描述符表的位置。
因此在使用完毕之后,就需要进行“释放”。
在前面的记录中,socket都没有释放,一方面这些socket生命周期更长(跟随整个程序),另一方面这些socket也不多,固定数量。
但是此处的clientSocket数量多,每个客户端都有一个,生命周期也更短。
- package network;
-
- import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
-
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.Socket;
- import java.util.Scanner;
-
-
- public class TcpEchoClient {
- //当前这里的socket既给服务器用,又给客户端用,
- private Socket socket = null;
- public TcpEchoClient(String serverIp,int serverPort) throws IOException {
- //Socket构造方法能够识别点分十进制格式的ip地址,比DatagramPacket更方便
- //new这个对象的同时,就会进行Tcp连接操作
- 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()) {
- while (true){
- //1.先从键盘上读取用户输入的内容
- System.out.print(" >");
- String request = scanner.next();
- if(request.equals("exit")){
- System.out.println("goodbye");
- break;
- }
- //2.把读到的内容构造成请求,发送给服务器
- PrintWriter printWriter = new PrintWriter(outputStream);
- printWriter.println(request);
- //此处加上flush,保证我们的数据确实发送出去了
- printWriter.flush();
- //3.读取服务器的响应,
- Scanner respScanner = new Scanner(inputStream);
- String response = respScanner.next();
-
- //4.把响应的内容给显示到界面上
- System.out.println(response);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoClient client = new TcpEchoClient("127.0.0.1",1111);
- client.start();
- }
- }
这个对象的构造过程,就是触发TCP建立连接的过程(打电话就开始拨号了),如果客户端没有这个代码,服务器就会在accept这里阻塞,无法产生出clientSocket了。
outputSteam相当于是对应着一个文件描述符表(socket文件),通过outputStream就可以往这个文件描述符表中写数据。
outputStream自身的方法不便写字符串,把这个流转换一下,用一个PrintWrite对象来表示(对应的文件描述符表还是同一个)
使用PrintWrite写和OutputStream写,是往同一个地方写,只不过写的过程更方便了。
TCP发送数据时,需要先建立连接,什么时候关闭连接,就决定是短连接还是长连接。
(1)短连接:客户端每次给服务器发消息,先建立连接,发送请求,读取响应,关闭连接。下次再发送,则重新建立加连接。
(2)长连接:客户端,建立连接之后,连接不着急断开,然后再发送请求,读取响应;若干轮之后,客户端确实短时间内不再需要这个连接了,此时就断开。
服务器这里的开发很少有不用多线程的情况(当然也有用多进程的)。
这里就可以对代码进行改进:
使用多线程版本处理程序,最大的问题就是可能会涉及到频繁的申请释放线程。
在多线程的基础上,使用线程池进行代码编写。
但是如果客户端非常多,而且客户端还迟迟不断开的高压,就回导致机器上会有很多线程。此时就就会提起一种方法:增加机器。但是这就意味着需要增加成本,需要多花钱,这里的问题又被称为是C10M问题。
C10K问题:单机处理1W个客户端。
C10M问题,单机处理1kw个客户端(针对多线程的版本,最大的问题就是机器承担不了这么大的线程开销,是否有办法一个线程,处理很多个客户端连接?)
IO多路复用/IO多路转接
给这个线程安排个集合,这个集合就放了一堆连接,线程就负责监听这个集合,这里的哪个连接有数据来,线程就处理哪个连接。虽然连接有很多,但是这些连接的请求并非严格意义的同时,总还是有先后的。
在操作系统中提供了一些原生的API select、poll、epoll 在Java中提供了一组NIO 这样 类,就封装了上述多路复用的API。
这一节和前一节的内容主要介绍的就是Udp和TcpSocket编程,需要注意的是,关于有无连接、面向字节流还是数据报以及全双工与否,这些特点在代码中都是有体现的。关于时候是可靠传输这一特点是隐藏在TCP背后,从代码的角度是感受不到的。TCP诞生的初心也就是为了解决可靠传输的问题。
下一部分将介绍网络原理知识的相关内容。
继续加油哦!