700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 网络编程套接字 socket 之 TCP

网络编程套接字 socket 之 TCP

时间:2022-04-19 18:42:48

相关推荐

网络编程套接字 socket 之 TCP

目录

一、TCPsocket api

ServerSocket 类

Socket 类

二、TCP回显服务器客户端

服务器

客户端

并发服务器

UDP与TCP对比:

一、TCPsocket api

我们来一起学习一下TCP socket api的使用,这个api与我们之前学习的IO操作紧密相关。

ServerSocket 类

顾名思义,ServerSocket是专门创建给 TCP服务器的 Socket对象

Socket 类

这里的 Socket类 既是客户端的Socket,也可以是服务器接收到客户端连接后,返回的服务器Socket,不论是那个Socket,都是双方建立连接后,保存对方信息,进行收发数据的。

在服务器这边,是由accept返回的,

在客户端这边,是我们代码构造的,构造的时候指定IP和端口号(此处的指定的IP和端口号是服务器的)有了这个消息就可以和服务器建立联系了。

​注意:TCP不需要一个类来表示“TCP”数据报,TCP是面向字节流的,是以字节的方式流式传输,不像UDP是以数据报为单位进行传输。

TCP长短连接

顾名思义,我们的TCP的长短连接,就表示我们TCP建立连接后,什么时候关闭连接就决定了是长连接还是短链接。

短连接: 在每次接收到数据并返回响应后,关闭连接。也就是说短连接只能收发一次数据。

长连接: 一直保持连接的状态,不关闭连接,双方可以不停的收发数据。

两者各有优缺,短连接适用于客户端请求频率不高的场景,浏览网页等。长连接适用于客户端与服务器频繁通信的场景,视频通话等。

二、TCP回显服务器客户端

服务器

已经有UDP实现的基础,今天TCP实现已经有的部分就比较容易理解了。

public class EchoServer {private ServerSocket serverSocket = null;public EchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}}

先创建TCP服务器Socket对象,并指定端口号。

//使用这个clientSocket和具体的客户端进行交流Socket clientSocket = serverSocket.accept();processConnection(clientSocket);

这个代码中,每次用到一个clientSocket,都会返回创建一个Socket对象,Socket就是文件,每次创建一个clientSocket对象,就要占用一个文件描述符表的位置。因此在使用完毕后,需要进行释放。

前面Udp的socket都没有释放,一方面是因为这些socket生命周期更长(跟随整个程序),另一方面是因为这些socket不多,固定数量,但是Tcp里的clientSocket数量多,生命周期短。

与客户端进行连接,如果没有获取到连接,则会发生阻塞等待,每获取到一个客户端连接就会返回一个Socket对象,该Socket对象是专门负责与连接的客户端进行通信的。

我们获取到的每一个Socket对象,可能会进行多次通信,所以我们获取到连接后,将通信封装成一个方法。

private void processCoonection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) { }catch (IOException e) {e.printStackTrace();}finally {try{clientSocket.close();}catch (IOException e) {e.printStackTrace();}}}

我们先获取与输出输入流,因为需要释放我们直接将它们放到try()中,然后在finally里释放Socket对象资源。

Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()) {//数据已经读完了System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}String request = scanner.next();

我们将输入流封装到Scanner里,从控制台输入,然后判断控制台是否还有数据输入,如果没有数据输入就退出,然后获取客户端的请求。

//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);

问: 当前代码是使用println来写入的,结果中带有一个\n换行,如果不用println而是用print(不带\n换行)这个代码是否还可以正确运行?

答:不行,TCP是面向字节流的协议,字节流的特性,一次读多少个字节都行,接收方如何知道这一次要读多少个字节,这需要我们在数据传输中进行明确约定,println是隐式约定了使用\n来作为当前代码的请求/响应分割约定。

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("服务器启动!");ExecutorService threadpool = Executors.newCachedThreadPool();while (true) {//使用这个clientSocket和具体的客户端进行交流Socket clientSocket = serverSocket.accept();//此处使用多线程来处理//这里的多线程版本的程序,最大的问题就是可能会涉及到频繁申请释放线程// Thread t = new Thread(()->{//processConnection(clientSocket);// });// t.start();//使用线程池threadpool.submit(()->{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 {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(6060);server.start();}}

客户端

public class EchoClient {private Socket socket = null;public EchoClient(String serverIp,int serverPort) throws IOException {//我们TCP的Socket对象能够识别点分十进制的IP//我们在创建对象的时候,就会与服务器进行连接socket = new Socket(serverIp,serverPort);}}

我们在new 这个对象的过程,就会触发TCP建立连接的过程(相当于开始拨号了),如果我们客户端没有这部分代码,那么服务器就会在accept进行阻塞等待。

public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {//我们TCP的Socket对象能够识别点分十进制的IP//我们在创建对象的时候,就会与服务器进行连接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) {System.out.print("> ");String request = scanner.next();if(request.equals("exit")) {System.out.println("客户端退出!");break;}PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(request);printWriter.flush();Scanner respScanner = new Scanner(inputStream);String response = respScanner.next();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",6060);client.start();}}

outputStream 相当于对应着一个文件描述符(socket文件),通过 outputStream 就可以往这个文件描述符中写数据了。

outputStream 本身的方法不方便写字符串,需要把这个流转换一下,用一个PrintWriter对象来表示(对应的文件描述符还是同一个)

使用PrintWriter写 和 使用outputStream 写,都是往同一个地方写,只不过写的过程更方便罢了~

并发服务器

我们看观察一下我们服务器的代码。

当我们客户端连接上这个服务器的时候,就执行到processConnection方法的while循环中,循环不结束,processConnection也不结束,我们的accpet就无法获取到第二个客户端socket对象。

那么我们如何解决这个问题呢?

使用多线程,我们的主线程专门负责进行accept,每收到一个连接,创建新线程,由新线程来负责处理这个新的客户端。

每个线程都是独立的执行流,每个独立的执行流是各自执行各自的逻辑,彼此之间是并发的关系,不会说这边阻塞影响到另一边的执行~

public void start() throws IOException {System.out.println("服务器启动!"); while (true) {//使用这个clientSocket和具体的客户端进行交流Socket clientSocket = serverSocket.accept();//此处使用多线程来处理//这里的多线程版本的程序,最大的问题就是可能会涉及到频繁申请释放线程Thread t = new Thread(()->{processConnection(clientSocket);});t.start(); }}

我们可以使用多线程来解决这个问题,但是现在每获取到一个连接就会创建一个线程,如果同一时刻连接过多,我们创建了大量线程,资源全部耗费在了线程切换上面了,我们可以使用线程池来提升效率。

public void start() throws IOException {System.out.println("服务器启动!");ExecutorService threadpool = Executors.newCachedThreadPool();while (true) {//使用这个clientSocket和具体的客户端进行交流Socket clientSocket = serverSocket.accept();//使用线程池threadpool.submit(()->{processConnection(clientSocket);});}}

尽管我们使用了线程池了,但还是不够,如果我们的客户端非常多,而且都迟迟不断开,就会导致我们会有很多线程,对于我们来说是一个很大的负担。

能否有办法解决单机支持更大量客户端的问题呢?也是经典的C10M(单机处理1KW个客户端)问题

这里并不是说单机真正能处理1KW个客户端,只是表达说客户端的量非常大,针对我们上述多线程的版本,我们的机器是承受不了这么多线程的开销的,那么是否有办法一个线程处理很多客户端连接呢? 这就是IO多路复用,IO多路转接技术

给线程安排一个集合,这个集合放了一堆连接,我们线程负责监听集合,那个连接有数据来了,就处理那个连接。虽然我们的连接有很多,但是我们这里的连接并不是严格意义上的同时,也是有先后的,我们的操作系统里,提供了一些API,比如select,poll,epoll,我们的java里,也提供了一组NIO这样的类,封装了上述技术。

相当于等烧水的时间里,去刷个牙,把时间利用起来~

UDP与TCP对比:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。