[线程与网络] 网络编程与通信原理(三):网络编程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多路复用.
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。