[线程与网络] 网络编程与通信原理(三):网络编程Socket套接字(TCP协议)

LileSily 2024-08-20 09:07:05 阅读 100

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343

🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482

🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482

🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482

🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482

感谢点赞与关注~~~

在这里插入图片描述

目录

1. TCP套接字编程1.1 API介绍1.1.1 ServerSocket1.1.2 Socket

1.2 代码实例1.2.1 回显服务器1.2.2 回显客户端

1.3 为服务器引入多线程

2. 拓展了解:长连接与短连接

1. TCP套接字编程

前面的UDP套接字是基于数据报为基本单位进行传输的,而今天我们要叙述的TCP套接字是基于字节为单位进行传输的.

1.1 API介绍

1.1.1 ServerSocket

本类是创建在服务器端的Socket的API.

构造方法:

方法签名 方法说明
ServerSocket(int port) 创建⼀个服务端流套接字Socket,并绑定到指定端⼝

常用成员方法

方法签名 方法说明
Socket accept() 开始接听指定端⼝(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待(没有客户端与之进行连接),注意这里返回的数据类型是Socket类型
void close() 关闭此套接字

举例说明:买房

有一天一位老哥想要买一套房子,他在马马路牙子上见到了一位西装革履的销售小哥,这时候这位销售小哥就问老哥:“哥,要买房吗?”.这位老哥正好想买套房子,于是跟着小哥去了售楼部.到了售楼部之后,这位小哥一挥手,来了一个Leader风格穿着的小姐姐,哥老哥一桶踢里哐啷地介绍房子.

在这个过程中,ServerSocket的构造方法的作用就好像是这个销售小哥一样,用来揽客.(用来和客户端用端口号建立连接),而这个售楼部的小姐姐就好像ServerSocket中的accept方法一样,在销售小哥揽客之后,接待客户(在与客户端建立连接之后,接通客户端的请求).

在这里插入图片描述

1.1.2 Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服

务端Socket,也就是Socket类在客户端和服务器中都要用.

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据

的。

Socket的构造方法

方法签名 方法说明
Socket(String host,int port) 创建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接

[注意] 客户端与服务器进行连接的时候,构造方法本身就可以与指定的服务器进行连接,也就是拨号的过程.

Socket的常用成员方法

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输⼊流
OutputStream getOutputStream() 返回此套接字的输出流

这里我们看到,在使用这些成员方法的时候,可以获取到Socket套接字的输入流和输出流,之后我们在客户端这边向着服务器发送请求的时候,使用的是输出流,反之从服务器获取响应的时候,我们用的是输入流,反之,在服务器这边,向着客户端返回响应的时候,使用输出流,接收请求的时候,使用输入流,这里的输入和输出的参照是以自身为参照.后续我们使用read()或者Scanner从InputStream读取数据之后,就可以对数据进行操作,处理好之后,在通过write()操作写回OutputStream.

[注意] 这里使用到的输入流和输出流,我们在之前的文件IO章节提到过.

https://blog.csdn.net/2301_80050796/article/details/138851572?spm=1001.2014.3001.5501

1.2 代码实例

在代码中,我们整体会涉及到三个Socket套接字:

服务器ServerSocket服务器Socket,通过这个Socket和客户端提供交换能力客户端Socket,通过这个Socket和服务器提供交换能力

1.2.1 回显服务器

<code>import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.Scanner;

public class TCP_Echo_Server {

private ServerSocket server = null;//服务器用于操作网卡的Socket

//构造方法指定端口号

public TCP_Echo_Server(int port) throws IOException {

server = new ServerSocket(port);//传入端口号,创建服务器Socket

}

public void start() throws IOException {

System.out.println("服务器启动");

while (true){

Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象

processConnection(clientSocket);//传入accept的请求,处理请求

}

}

private void processConnection(Socket cilentSocket) throws IOException {

//打印登录日志

System.out.printf("[%s:%d] 客户端上线\n",cilentSocket.getInetAddress(),cilentSocket.getPort());

//从获取到的请求中获取到输入流和输出流,由于它们两个需要关闭,所以写在try中

try (InputStream inputStream = cilentSocket.getInputStream();

OutputStream outputStream = cilentSocket.getOutputStream()){

while (true){

Scanner scanner = new Scanner(inputStream);//使用Scanner的方式读取输入流中的数据

if (!scanner.hasNext()){ //如果输入流中没有数据了,就结束循环

System.out.printf("[%s:%d] 客户端下线\n",cilentSocket.getInetAddress(),cilentSocket.getPort());

break;

}

String request = scanner.next();//读取字符串

String response = process(request);//处理请求,返回响应

outputStream.write(response.getBytes());//把响应写回输出流

System.out.printf("[%s:%d] req = %s,resp = %s\n",cilentSocket.getInetAddress(),cilentSocket.getPort(),

request,response);//打印请求响应日志

}

}finally {

cilentSocket.close();//关闭Socket.每一个客户端请求都有一个Socket,用完就没用了,所以要关闭

}

}

private String process(String request){

return request + '\n';//之所以要加上一个换行符,是因为响应写回到客户端要把一大串字节分为若干数据报

}

public static void main(String[] args) throws IOException {

TCP_Echo_Server tcpEchoServer = new TCP_Echo_Server(9090);

tcpEchoServer.start();

}

}

[注意的几点]

注意在accept接听请求的时候,在外层加上while死循环.服务器在通过ServerSocket接听客户端发送来的请求并读取数据之后,需要关闭cilentSocket对象,而且为了防止程序在处理请求的过程中出现异常导致程序以外终止,我们还需要把它放入finally中.不关闭就会导致文件资源泄露,是当前的cilentSocket对象的文件描述符表得不到释放.

我们还是那上面的买房的例子来说明

销售小哥不可能只揽你一个人,他需要不停地从外面揽客,就需要不停有小姐姐来接待,所以需要while死循环.而售楼部的小姐姐也不可能光给你自己提供服务,而是在接待完你之后,还会在接待其他人,所以在接待你之后,就需要close.

就像我们前面说的,使用InputStream和OutputStream之后需要关闭,所以我们就需要把输入流和输出流的创建写入try的括号中.这里不使用read()读取,读取出来的是字节byte[],使用Scanner进行读取的时候,会对字节进行转换,自动转换为字符串.scanner.next()读取数据的时候,由于我们后边写到的客户端会使用"空白符"来将一大串数据进行分割,每次分割都对应的是一个应用层数据报,所以我们这里在使用next读取的时候,读取到的就是一个个完整的"字节流请求",而不是没有分隔符的"一大坨数据".与第5条相同的道理,在返回响应的时候,我们同样要在数据的末尾加上"空白符",以便客户端进行读取. return request + '\n';

1.2.2 回显客户端

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.Socket;

import java.util.Scanner;

public class TCP_Echo_Client {

private Socket socket = null;//客户端用于操作网卡的Socket

public TCP_Echo_Client(String IP,int Port) throws IOException {

socket = new Socket(IP,Port);//通过构造方法传入的服务器的IP地址和端口号构造Socket

}

public void start() throws IOException {

System.out.println("客户端启动");

Scanner scanner = new Scanner(System.in);//客户端通过控制台输入请求

try (InputStream inputStream = socket.getInputStream();

OutputStream outputStream = socket.getOutputStream()){

while (true){

System.out.println(">>");

String request = scanner.next();//通过Scanner获取到要发送的请求

request += '\n';//为请求加上一个空白符,以便服务器通过Scanner.next读取

outputStream.write(request.getBytes());//写入输出流

Scanner scanner1 = new Scanner(inputStream);//从输入流读取服务器响应

String response = scanner1.next();

System.out.println(response);

}

}

}

public static void main(String[] args) throws IOException {

TCP_Echo_Client tcpEchoClient = new TCP_Echo_Client("127.0.0.1",9090);

tcpEchoClient.start();

}

}

[需要注意的几点]

request += '\n';这里要在请求的后面加上\n,这样在服务器读取响应的时候,就是一个个完整的"字节流请求".scanner1.next();读取"字节流响应"的时候,由于前面服务器使用\n对数据进行过分割,所以我们在这里就可以使用这样的方式进行读取.

1.3 为服务器引入多线程

上面的代码出现的最大的问题就是,不可以同时给多个客户端提供服务.这其实不是API的问题,是我们代码结构的问题,就是由于我们在上述的代码中出现了死循环嵌套的代码方式:

第一个客户端在被服务器accept()接听之后,进入processConnection(clientSocket);方法内部会有while (true)循环,这就使得服务器在没有等到第一个客户端结束运行之后,上层start()方法中的死循环无法进行下一个循环接听下一个客户端.所以会出现不支持同时给多个客户端提供服务的现象.

在这里插入图片描述

先启动的客户端有响应.

在这里插入图片描述

后启动的客户端无响应.

所以我们在服务器的代码中引入了多线程.每<code>accept()接听一个客户端,就给这个客户端的Socket单独创建一个线程,这样在接听下一个客户端的时候,就不会受上一个客户端的影响.

引入多线程

public void start() throws IOException {

System.out.println("服务器启动");

while (true){

Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象

Thread thread = new Thread(()->{ //使用多线程同时处理多个客户端发来的请求

try {

processConnection(clientSocket);//传入accept的请求,处理请求

} catch (IOException e) {

throw new RuntimeException(e);

}

});

thread.start();

}

}

其中每个创建的新线程为每个客户端的Socket提供服务,而main线程用来循环处理接听操作.

当然我们也可以引入线程池,我们在前面提到过线程池的概念.

https://blog.csdn.net/2301_80050796/article/details/138388735?spm=1001.2014.3001.5501

从下图可以看到,同时启动的两个客户端都有反应:

在这里插入图片描述

在这里插入图片描述

服务器也有两个客户端的登录日志:

在这里插入图片描述

引入线程池

<code> public void start() throws IOException {

System.out.println("服务器启动");

ExecutorService executorService = Executors.newCachedThreadPool();//使用线程池同时处理多个客户端发来的请求

while (true){

Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象

executorService.submit(new Runnable() {

@Override

public void run() {

try {

processConnection(clientSocket);//传入accept的请求,处理请求

} catch (IOException e) {

throw new RuntimeException(e);

}

}

});

}

}

[注意]

这里我们使用线程数量动态变化的线程池创建方式.Executors.newCachedThreadPool();,而使用创建固定线程的线程池创建方式不合乎逻辑,我们不可能只给固定几个客户端提供服务.这里我们使用线程池的方式,比循环创建线程的方式更加高效,创建线程会涉及到线程的创建和销毁,会有一定的开销,而线程池在用完一个线程之后,就会将这个线程回收回线程池,使得每一个线程更加充分地利用起来.

2. 拓展了解:长连接与短连接

TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:

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

数据。

长连接:不关闭连接,⼀直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可

以多次收发数据。

基于BIO(同步阻塞IO)的长连接会⼀直占⽤系统资源。对于并发要求很⾼的服务端系统来说,这样的消耗是不能承受的。

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在⼀个线程中运⾏

⼀次阻塞等待对应着⼀次请求、响应,不停处理也就是⻓连接的特性:⼀直不关闭连接,不停的处理

请求。

实际应⽤时,服务端⼀般是基于NIO(即同步非阻塞IO或IO多路复用)来实现长连接,性能可以极大的提升.一个线程在同时管理多个clientSocket.

举例说明:下楼上小摊买饭

一家三口,爸爸妈妈和孩子.三个人要吃不同的饭,第一种方案就是,三个人都下楼,各买各的.就是同步阻塞IO.

第二种方案就是只让爸爸一个人下去买,爸爸就可以在买完一份饭之后,在等待这个饭做出来的途中,再去买另一个饭,这样就会大大提高效率.就是IO多路复用.

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。