提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
选择代码,Ctrl+Alt+t可以触发包围模式,快速捕获异常
1. 发展历史


中间的是交换机,最后那个是路由器

移动互联网就是手机
广域网,就是所有人构成的一个网络
2. 重要概念
2.1 IP地址
IP地址主要⽤于标识⽹络主机、其他⽹络设备(如路由器)的⽹络地址。简单说,IP地址⽤于定位主机的⽹络地址。
输入ipconfig-》可获取ip地址

2.2 端⼝号
在⽹络通信中,IP地址⽤于标识主机⽹络地址,端⼝号可以标识主机中发送数据、接收数据的进程。简单说:端⼝号⽤于定位主机中的进程

2.3 认识协议
协议,⽹络协议的简称,⽹络协议是⽹络通信(即⽹络数据传输)经过的所有⽹络设备都必须共同遵从的⼀组约定、规则。如怎么样建⽴连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。
协议(protocol)最终体现为在⽹络上传输的数据包的格式
计算机之间的传输媒介是光信号和电信号
2.4 协议分层
为什么需要⽹络协议的分层?
分层最⼤的好处,类似于⾯向接⼝编程:定义好两层间的接⼝规范,让双⽅遵循这个规范来对接。在代码中,类似于定义好⼀个接⼝,⼀⽅为接⼝的实现类(提供⽅,提供服务),⼀⽅为接⼝的使⽤类(使⽤⽅,使⽤服务):
• 对于使⽤⽅来说,并不关⼼提供⽅是如何实现的,只需要使⽤接⼝即可
• 对于提供⽅来说,利⽤封装的特性,隐藏了实现的细节,只需要开放接⼝即可。
2.5 OSI七层模型
OSI:即Open System Interconnection,开放系统互连
• OSI 七层⽹络模型是⼀个逻辑上的定义和规范:把⽹络从逻辑上分为了7层。
• OSI 七层模型是⼀种框架性的设计⽅法,其最主要的功能使就是帮助不同类型的主机实现数据传输;


OSI 七层模型既复杂⼜不实⽤:所以 OSI 七层模型没有落地、实现
2.6 TCP/IP五层(或四层)模型
TCP/IP是⼀组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。
TCP/IP通讯协议采⽤了5层的层级结构,每⼀层都呼叫它的下⼀层所提供的⽹络来完成⾃⼰的需求。
• 应⽤层:负责应⽤程序间沟通,如简单电⼦邮件传输(SMTP)、⽂件传输协议(FTP)、⽹络远程访问协议(Telnet)等。我们的⽹络编程主要就是针对应⽤层。
• 传输层:负责两台主机之间的数据传输。如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到⽬标主机。
• ⽹络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识⼀台主机,并通过路由表的⽅式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)⼯作在⽹路层。
• 数据链路层:负责设备之间的数据帧的传送和识别。例如⽹卡设备的驱动、帧同步(就是说从⽹线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯作。有以太⽹、令牌环⽹,⽆线LAN等标准。交换机(Switch)⼯作在数据链路层。
• 物理层:负责光/电信号的传递⽅式。⽐如现在以太⽹通⽤的⽹线(双绞 线)、早期以太⽹采⽤的的同轴电缆(现在主要⽤于有线电视)、光纤,现在的wifi⽆线⽹使⽤电磁波等都属于物理层的概念。物理层的能⼒决定了最⼤传输速率、传输距离、抗⼲扰性等。集线器(Hub)⼯作在物理层。


应用层,就是决定把数据拿来干嘛的

物理层我们考虑的⽐较少。因此很多时候也可以称为 TCP/IP四层模型
⽹络设备所在分层
• 对于⼀台主机,它的操作系统内核实现了从传输层到物理层的内容,也即是TCP/IP五层模型的下四层;
• 对于⼀台路由器,它实现了从⽹络层到物理层,也即是TCP/IP五层模型的下三层;
• 对于⼀台交换机,它实现了从数据链路层到物理层,也即是TCP/IP五层模型的下两层;
• 对于集线器,它只实现了物理层;
注意我们这⾥说的是传统意义上的交换机和路由器,也称为⼆层交换机(⼯作在TCP/IP五层模型的下两层)、三层路由器(⼯作在TCP/IP五层模型的下三层)。
随着现在⽹络设备技术的不断发展,也出现了很多3层或4层交换机,4层路由器。我们以下说的⽹络设备都是传统意义上的交换机和路由器。


2.7 封装和分⽤
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在⽹络层叫做数据报(datagram),在链路层叫做帧(frame)。
• 应⽤层数据通过协议栈发到⽹络上时,每层协议都要加上⼀个数据⾸部(header),称为封装(Encapsulation)。
• ⾸部信息中包含了⼀些类似于⾸部有多⻓,载荷(payload)有多⻓,上层协议是什么等信息。
• 数据封装成帧后发到传输介质上,到达⽬的主机后每层协议再剥掉相应的⾸部,根据⾸部中的 “上层协议字段” 将数据交给对应的上层协议处理
下图为数据封装的过程

下图为数据分⽤的过程

例子

















3. ⽹络编程套接字
操作系统给应用程序(传输层到应用层)提供的API,就叫socket api(java版本)
UDP和TCP都有一套,socket api
UDP和TCP区别
TCP有连接,可靠传输,面向字节流,全双工
UDP:无连接,不可靠传输,面向数据报,全双工
有无连接:通信双方是否保存对端的信息,保存就是有连接,不保存对端的信息就是无连接
可靠传输:信息尽可能到达对方,有重传机制,对方没有收到就重传,更加复杂,效率低
不可靠传输:完全不考虑数据能否到达对方,到不到达不管的,没有重传
面向字节流:传输过程就和文件流是一样的特点

面向数据报:传输数据的基本单位不是字节,而是UDP数据报,一次发送接收,必须发送接收一个完整的UDP数据报
全双工:一个通信链路,可以发送数据,也可以接收数据,一个网线里面有8根铜线,可以全双工
半双工:一个通信链路,只能发送数据或者接收数据(单向通信)
4. UDP数据报套接字编程
4.1 DatagramSocket和DatagramPacket
DatagramSocket 是UDP Socket,⽤于发送和接收UDP数据报。代表一个socket对象,是操作系统下一种广义的文件类型,这种文件就是网卡这种硬件设备
代码不好直接操作网卡,操作系统把网卡概念分装为socket,这样就可以操作socket去操作网卡了
DatagramPacket代表一个UDP数据报,UDP传输的基本单位
DatagramPacket构造方法
DatagramSocket()创建⼀个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端⼝(⼀般⽤于客⼾端)
DatagramSocket(int port) 创建⼀个UDP数据报套接字的Socket,绑定到本机指定的端⼝(⼀般⽤于服务端)
DatagramPacket方法
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该
⽅法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发
送)
void close() 关闭此数据报套接字
DatagramPacket构造方法
DatagramPacket(byte[] buf, int length) 构造⼀个DatagramPacket以⽤来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定⻓度(第⼆个参数length)
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address)
构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓度(第⼆个参数length)。address指定⽬的主机的IP和端⼝号
DatagramPacket方法
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端⼝号;或从发送的数据报中,获取接收端主机端⼝号
byte[] getData() 获取数据报中的数据
代码实现需要两个程序,一个客户端,一个服务端,主动发起通信:客户端,被动接受:服务端
4.2 回显服务器
客户端发啥,服务器就返回啥
public class UDPServer {
private DatagramSocket socket = null;
public UDPServer(int port) throws SocketException {
//服务器socket需要指定端口号,这样客户端才能找到--->进程和api端口号绑定了
// 同一时刻,一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号,创建多个socket,多个进程不能绑定同一个端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//启动之后,不停处理客户端发来的请求,随时准备好,客户端来消息--》处理
while (true){
//需要指定DatagramPacket需要存储数据的字节数组和数组长度
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
//1,读取客户端的请求并解析,receive从网卡上的对应端口号读取数据,没有数据的话,就会阻塞等待
// 也是通过一个输出型参数DatagramPacket来获取网卡上的数据的
socket.receive(requestPacket);
// 上述收到的数据都是二进制byte[]的形式--->转为String,getData获取字节数组,getLength是数据包的长度
String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
//2,根据请求,计算响应,
String response = process(request);
//3. 返回给客户端,responsePacket必须是有内容的,不能为空的字节数组了,长度不能写response.length,因为response的是字符个数,编码方方式要注意
//都是英文字母---》没事,有中文字符,utf8---》有问题了,一个中文,三个字节
//因为UDP无连接,所以DatagramSocket不持有对方的ip和端口号,所以发送的时候DatagramPacket还要指定端口号和ip
//requestPacket.getSocketAddress()从数据包中就可以得到对方的ip和端口号了
DatagramPacket responsePacket = new DatagramPacket
(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d],req=%s,res=%s \n",requestPacket.getAddress(),requestPacket.getPort(),request,response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UDPServer server = new UDPServer(9090);
server.start();
}
}
socket这个文件也是需要关闭的—》但是这里是进程结束了,socket才关闭
public class UDPClient {
DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UDPClient(String serverIP,int ServerPort) throws SocketException {
socket = new DatagramSocket();//没有指定端口号---》默认一个随机的无人使用的端口号,因为服务器是被动接受,所以服务器要指定端口号才找得到
this.serverIP = serverIP;//这是服务器的ip和端口号
this.serverPort = ServerPort;
}
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while (true) {
//1.从用户读取用户的输入
System.out.println("-> ");
String request = scanner.next();
//构造UDP请求,发送DatagramPacket中带有目标的ip和端口号
// DatagramPacket packet = new DatagramPacket(request.getBytes(), request.getBytes().length,this.serverIP,this.serverPort);
//这个构造DatagramPacket不支持,因为我们这里输入的ip是点分十进制的ip,但是它需要的ip是一个整数,所以要转换一下InetAddress.getByName
DatagramPacket packet = new DatagramPacket
(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIP),this.serverPort);
socket.send(packet);
/// 3.从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//4.把响应打印到控制台上
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UDPClient udpClient = new UDPClient("127.0.0.1",9090);
udpClient.start();
}
}
先启动服务器,在启动客户端


String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
这里可以指定编码,默认就是utf8,所以打印出来的中文是正常的
文件那里,字节流,(char)c---->不能获取正常中文,因为utf8是三个字节,这里值转换了一个字节,所以如果要正常显示的话,也必须指定编码为utf8
字符流的话—》把utf8的三个字节弄为一个字节了–》所以(char)c能正常获取到中文
socket.receive(requestPacket);
每次都会在这里阻塞等待
如果要跨主机的话-----》必须连同一个wifi才可以–》同一个局域网
或者服务器代码放在云服务器上–》访问公网ip
//实现一个英译汉
public class UdpDictServer extends UDPServer{
private Map<String,String> dict= new HashMap<String,String>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("pig","小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"未知单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}


4.3 端口号被占用
netstat -ano
可以看到所有端口号的进程和进程id
netstat -ano | findstr 9090
这里可以查看指定的端口号得到进程
netstat -ano | grep 9090
linux上就是grep了
同一个协议下,一个端口号只能被一个进程绑定
9090端口,在UDP下被一个进程绑定了,还可以在TCP下被另一个进程绑定
5. TCP流套接字编程
5.1 ServerSocket和Socket
ServerSocket:专门给服务器使用
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造⽅法:
Socket
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。
这里没有TCP数据包,这种基本单位,TCP是面向字节流的,传输基本单位就是byte
UDP是面向数据报:所以需要定义专门的类,表示UDP数据报,作为UDP传输的基本单位
所以ServerSocket和Socket都是socket
ServerSocket 构造⽅法:
ServerSocket(int port) 创建⼀个服务端流套接字Socket,并绑定到指定端⼝
ServerSocket ⽅法:
Socket accept() 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客⼾端的连接,否则阻塞等待
void close() 关闭此套接字
Socket 构造⽅法:
Socket(String host, int port) 创建⼀个客⼾端流套接字Socket,并与对应IP的主机
上,对应端⼝的进程建⽴连接,构造这个对象,就直接去建立连接了
Socket ⽅法:
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输⼊流,这是字节流
OutputStream getOutputStream() 返回此套接字的输出流
5.2 回显服务器
public class TCPServer {
private ServerSocket serverSocket=null;
public TCPServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
processConnection(clientSocket);//其中一个连接的处理
}
}
//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) {
System.out.printf("[%s,%d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
//获取socket中持有的流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream)) {
//tcp是全双工的,既可以读也可以写
//使用Scanner来包装InputStream
while (true) {
// 1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//如果scanner中无法读取数据,说明客户端关闭了连接了,因为scanner会一直阻塞等待客户端的输入的,hasNext说明客户端ctrl+c了和C语言一样
break;
}
String request = scanner.next();//请求应该是以空白符,换行符等结尾
//scanner.nextLine()要求必须带有\n
//2.根据请求计算响应
String response = process(request);
//3.返回客户端
// outputStream.write(response.getBytes());//用字节数组的形式来写入
//println在写入响应的时候会换行---》客户端收到的时候scanner.next(),就会知道这次输入结束了,不然会一直等着什么时候服务端返回结束
printWriter.println(response);
//4.打印日志
System.out.printf("[%s,%d],req=%s,res=%s\n",
clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPServer tcpServer = new TCPServer(9090);
tcpServer.start();
}
}
public class TCPClient {
private Socket socket=null;
public TCPClient(String serverIp,int ServerPort) throws IOException {
socket = new Socket(serverIp,ServerPort);//要指定服务器的ip和端口,一旦new好,就会和服务器建立连接,而且可以直接填写Stringip,不用转换
}
public void start(){
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in) ;
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
//1.从控制台读取数据
System.out.print("->");
String request = scannerIn.next();
//2.把请求发送到服务器
printWriter.println(request);
//3.从服务器读取响应
if (!scanner.hasNext()){
break;
}
String response = scanner.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPClient tcpClient =new TCPClient("127.0.0.1",9090);
tcpClient.start();
}
}
if (!scanner.hasNext()){
这里会阻塞:等待客户端发送数据,等待客户端println
在客户端发数据之前,hasNext是不会返回值的,会一直阻塞,没有HasNext的话,Next也是会阻塞的。除非客户端主动退出—》HasNext才会返回
serverSocket.accept()
这里也会阻塞


然后进行测试
客户端虽然建立了连接,但是客户端发送数据之后没有响应—》客户端没有发出去请求
printWriter.println(response);
像PrintWriter这种类,以及很多IO流中的泪,都是自带缓冲区的
引入缓冲区之后,进行写入数据的操作,不会立即出发IO,而是会先放在内存缓冲区中,等缓冲区放了很多之后,在统一进行发送
缓冲区不好放满,因为我们的数据太少了–》刷新缓冲区
flush操作
printWriter.println(response);
printWriter.flush();
改为这样就可以了
然后客户端和服务器都要加flush


这样就成功了
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
还有要注意这里,UDP的socket的生命周期是跟随整个进程的,进程没了,它就没了
但是TCP的clientSocket 这个,客户端断开连接了,这个socket就不在使用了,即使是同一个客户端重新连接–》也是一个新的socket----》这个在服务器上的客户端的socket是不会随着客户端的关闭,然后服务器上的clientSocket 是自动关闭的—》文件资源泄露–》所以我们要手动关闭
}finally {
System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();
}
这个是一定会执行的
还有一个问题就是一个服务器怎么同时给多个客户端提供服务呢
我们的server可以吗,我们的不可以,因为是但线程的,我们实现的这个start是单线程的,每次都只有一个才能连接成功,只有这个连接没了,才能退出processConnection,退出循环,才能到accept等待下一个客户端连接
首先怎么启动多个客户端呢


勾选允许多个实例
这样点击运行,就是自动运行多个实例了

但是客户端上线只有一个,所以不能多个连接的
但是我们关闭一个客户端,另一个客户端就可以连接了,因为另一个客户端一直等着连接呢
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
Thread thread = new Thread(()->{
try {
processConnection(clientSocket);//其中一个连接的处理
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
这样就可以了

这样就可以了
还可以优化–》线程池,万一突然来了很多客户端呢,然后连一会儿就断开了,后面又要重新创建线程
ExecutorService executorService = Executors.newCachedThreadPool();
//使用线程池
executorService.submit(()->{
try {
processConnection(clientSocket);//其中一个连接的处理
} catch (IOException e) {
throw new RuntimeException(e);
}
});
newCachedThreadPool的最大线程数是一个很大的数
但是万一来了几百万个客户端呢–》几百万个线程–》机器不行了—》使用IO多路复用
IO多路复用就是在网络服务器开发得到时候,使用很少的线程处理很多的客户端
一个线程可以给多个客户端提供服务(因为一个客户端要阻塞,所以一个线程搞得赢处理多个)
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in) ;
PrintWriter printWriter = new PrintWriter(outputStream);
scanner 和printWriter 要close吗,这个没有放在try里面啊
可以不放在try里面,因为它们就是分装的inputStream 和outputStream,文件描述符就是inputStream 和outputStream持有的
或者scanner 关闭也可以,它关闭也会顺便关闭inputStream
Scanner scannerIn = new Scanner(System.in) ;这个要关闭吗
一个进行中有三个特殊流对象特殊的文件描述符是不需要关闭的
这三个是进程一启动,操作系统自动打开的,生命周期跟随整个进程
System.in,System.out,System.err
就是这三个


完整代码
public class TCPClient {
private Socket socket=null;
public TCPClient(String serverIp,int ServerPort) throws IOException {
socket = new Socket(serverIp,ServerPort);//要指定服务器的ip和端口,一旦new好,就会和服务器建立连接,而且可以直接填写Stringip,不用转换
}
public void start(){
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in) ;
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
//1.从控制台读取数据
System.out.print("->");
String request = scannerIn.next();
//2.把请求发送到服务器
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应
if (!scanner.hasNext()){
break;
}
String response = scanner.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPClient tcpClient =new TCPClient("127.0.0.1",9090);
tcpClient.start();
}
}
public class TCPServer {
private ServerSocket serverSocket=null;
public TCPServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
ExecutorService executorService = Executors.newCachedThreadPool();
// Thread thread = new Thread(()->{
// try {
// processConnection(clientSocket);//其中一个连接的处理
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
// thread.start();
//使用线程池
executorService.submit(()->{
try {
processConnection(clientSocket);//其中一个连接的处理
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s,%d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
//获取socket中持有的流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream)) {
//tcp是全双工的,既可以读也可以写
//使用Scanner来包装InputStream
while (true) {
// 1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//如果scanner中无法读取数据,说明客户端关闭了连接了,因为scanner会一直阻塞等待客户端的输入的,hasNext说明客户端ctrl+c了和C语言一样
break;
}
String request = scanner.next();//请求应该是以空白符,换行符等结尾
//scanner.nextLine()要求必须带有\n
//2.根据请求计算响应
String response = process(request);
//3.返回客户端
// outputStream.write(response.getBytes());//用字节数组的形式来写入
//println在写入响应的时候会换行---》客户端收到的时候scanner.next(),就会知道这次输入结束了,不然会一直等着什么时候服务端返回结束
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s,%d],req=%s,res=%s\n",
clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPServer tcpServer = new TCPServer(9090);
tcpServer.start();
}
}
6. ⻓短连接
TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发数据。
⻓连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是⻓连接。也就是说,⻓连接可以多次收发数据。
对⽐以上⻓短连接,两者区别如下:
• 建⽴连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时的,⻓连接效率更⾼。
• 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送请求,也可以是服务端主动发。
• 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等
短连接:频繁建立连接----》用得少了
1065

被折叠的 条评论
为什么被折叠?



