目录
[1. 网络发展史](#1. 网络发展史)
[2. 网络通信的基础](#2. 网络通信的基础)
[2.1 IP地址](#2.1 IP地址)
[2.2 端口号](#2.2 端口号)
[2.3 协议](#2.3 协议)
[2.4 五元组](#2.4 五元组)
[2.5 协议分层](#2.5 协议分层)
[2.6 OSI七层模型](#2.6 OSI七层模型)
[2.7 TCP/IP五层(四层)协议](#2.7 TCP/IP五层(四层)协议)
[2.8 网络设备所在的分层](#2.8 网络设备所在的分层)
[2.9 网络数据通信的基本流程](#2.9 网络数据通信的基本流程)
[2.9.1 封装](#2.9.1 封装)
[2.9.2 应用程序获取到用户的输入,然后构造出来一个应用层的数据包。](#2.9.2 应用程序获取到用户的输入,然后构造出来一个应用层的数据包。)
[2.9.3 传输层拿到数据后,构造出来一个传输层的数据段。](#2.9.3 传输层拿到数据后,构造出来一个传输层的数据段。)
[2.9.4 网络层接收到数据后,构造出来一个网络层的数据报。](#2.9.4 网络层接收到数据后,构造出来一个网络层的数据报。)
[2.9.5 数据链路层接收到数据后,构造出来一个数据链路层的数据帧](#2.9.5 数据链路层接收到数据后,构造出来一个数据链路层的数据帧)
[2.9.6 网卡将这些数据传输出去](#2.9.6 网卡将这些数据传输出去)
[2.9.7 分用](#2.9.7 分用)
[2.9.8 数据到达接收方的网卡](#2.9.8 数据到达接收方的网卡)
[2.9.9 数据链路层接收到二进制数据](#2.9.9 数据链路层接收到二进制数据)
[2.9.10 网络层接收到以太网载荷数据](#2.9.10 网络层接收到以太网载荷数据)
[2.9.11 传输层接收到IP载荷数据](#2.9.11 传输层接收到IP载荷数据)
[2.9.12 应用层接收到TCP载荷数据](#2.9.12 应用层接收到TCP载荷数据)
[3. 网络编程](#3. 网络编程)
[3.1 什么是网络编程](#3.1 什么是网络编程)
[3.2 网络编程的基本概念](#3.2 网络编程的基本概念)
[4. 套接字(Socket)](#4. 套接字(Socket))
[5. UDP数据报套接字编程](#5. UDP数据报套接字编程)
[5.1 DatagramSocket类](#5.1 DatagramSocket类)
[5.2 DatagramPacket类](#5.2 DatagramPacket类)
[5.3 InetSocketAddress类](#5.3 InetSocketAddress类)
[5.4 代码实现](#5.4 代码实现)
[6. 基于UDP协议实现一个简单翻译服务器](#6. 基于UDP协议实现一个简单翻译服务器)
[7. TCP流套接字编程](#7. TCP流套接字编程)
[7.1 ServerSocket类](#7.1 ServerSocket类)
[7.2 Socket类](#7.2 Socket类)
[7.3 代码实现](#7.3 代码实现)
1. 网络发展史
网络经历了单机时代,局域网时代,广域网时代,移动互联网时代的过程。
**独立模式:**早期的计算机都是相互独立的,彼此之间不能进行互相通信,共享软件和数据。
**网络互连:**将多台计算机连接在一起,完成数据共享,数据共享本质就是网络数据传输,也就是说计算机之间通过网络来传输数据,也被成为网络通信。
根据网络互联的规模大小分为局域网和广域网。
**局域网LAN:**也就是局部组建的私有网络,处于局域网内的设备之间可以进行网络通信,而局域网与另一个局域网之间不能互相通信。
局域网组建网络的方式:
- 直接通过网线连接
- 基于集线器组建
- 基于交换机组建
- 基于交换机和路由器组建
**路由器:**本身就可以有网线接口和一个WAN接口(连接运营商的网线口)其他设备可以通过网线来连接路由器。
**交换机:**是拓展路由器上面的网线接口。一个接口连接路由器,其他接口连接设备。
**广域网WAN:**通过路由器将多个局域网连接起来,就形成了规模很大的广域网。
2. 网络通信的基础
2.1 IP地址
网络互连的目的就是为了网络通信,也就是为了让不同设备之间能够通过网络来传输数据,不同设备的进程之间能够传输数据。
如何确定是哪台机器发送的数据到另外一台机器上的?
这里就用到了IP地址来进行标识。
IP地址主要是用于表示网络主机和其他设备的网络地址,能够标识一台主机的网络地址,通过这个IP地址就可以找到这台主机。
IP地址是由32位二进制位(4个字节)组成的,通过 . 分成四组,每组8位二进制位,如:
10000000.01010101.10100010.10100101,通常在表示IP地址时候使用的是十进制来表示,每位都是(0~255之间的数字)例如:198.7.2.7。
2.2 端口号
在网络通信中,IP地址是用来表示某个具体的主机的网络地址,而端口号是用来表示 发送数据和接收数据的进程的,可以用来区分主机上的多个进程。
端口号范围是0~65535,在网络通信中,每个进程可以通过绑定一个端口号来发送或者接收数据,没有进行网络通信,不会分配端口号。
2.3 协议
当我们有了IP地址和端口号之后,就可以定位到哪两个主机的哪两个进程之间进行网络通信,网络通信传输的数据是二进制(1/0)数据,而我们发送的数据包含很多种类,包括:图片,视频,文本文件等,这些数据的格式也可能不同,如何告诉接收数据的进程,这些数据是什么意思呢?
此时就需要在网络通信中,规定一套规则也就是协议来规定传输的数据的格式。
协议也就是网络协议,是所有进行网络通信的设备都必须遵守的一套规则,通过这套规则才能实现真正的网络通信。
协议最终表现在网络通信中传输的数据包的形式。
2.4 五元组
进行一次网络通信,涉及到五个重要的信息,这五个信息被称为五元组。
**源IP:**发送数据的设备的IP地址,标识数据的来源。
**源端口号:**发送数据的进程的端口号。
**目的IP:**接收数据的设备的IP地址,标识数据的去向。
**目的端口号:**接受数据的进程的端口号。
**传输层协议:**数据传输使用的协议。
比如:电脑 A(源IP 192.168.1.100)的浏览器(源端口 56789)通过 TCP 协议(传输层协议)访问服务器 B(目的IP 203.0.113.5)的 HTTP 服务(目的端口 80),这五个参数组合就是唯一的五元组。
2.5 协议分层
网络通信是一个非常复杂的过程,如果我们只定义一个协议来规范网络通信的过程,这个协议会变得非常复杂,我们为了简化这个协议,将协议分成了很多层,相邻两层协议之间可以进行交互,上层协议可以调用下层协议,下层协议给上层协议提供服务,不能跨层交互。
分层设计的优点:
各层之间独立开发,互不影响,更新某一层,其他层不用修改。
上层协议不用了解下层协议是如何实现的,只用使用下层协议提供的接口就行。
降低了开发的复杂度,不同的厂商只需要按照协议开发一层就行。
问题定位更高效,逐层排查。
2.6 OSI七层模型
OSI七层模型将网络通信分成了七层。
**应用层:**直接面向用户使用的,提供了具体的网络服务(http协议等)。
**表示层:**处理数据格式转换,加密解密,压缩解压。
**会话层:**建立,管理和终止通信回话,协调通信节奏。
**传输层:**提供数据端和数据端的传输,通过端口号来区分进程(TCP,UDP协议)。
**网络层:**地址的管理(IP协议),路由的选择。
**数据链路层:**处理相邻设备间的帧传输(以太网协议)。
**物理层:**负责物理介质传输(网线,光纤)。
上面这七层协议是理论上的。
2.7 TCP/IP五层(四层)协议
实际进行网络通信时候使用的是:TCP/IP五层(四层)网络协议。
**应用层:**直接面向用户提供网络服务,负责应用程序间的沟通。核心协议:https(网页),FTP(文件传输)等。
**传输层:**提供端到端的数据传输,衔接网络层和应用层,任意两个设备之间进行数据传输,不考虑中间过程,只考虑起点和终点。核心协议:TCP协议,UDP协议。
**网络层:**两个任意设备之间是如何传输数据的,管理地址和选择路由。核心协议:IP协议,路由协议。
**数据链路层:**基于物理层,处理相邻设备间的帧传输,例如:电脑和路由器/交换机之间的传输。核心协议:以太网协议。
**物理层:**负责光信号或者电信号的传输。常见设备:网线,光纤等。
有些地方会把数据链层和物理层作为一个整体,都是硬件设备相关的,所以就有了TCP/IP四层协议。
2.8 网络设备所在的分层
对于一台主机 ,操作系统实现了从物理层到传输层的封装分用的过程。
对于一台路由器 ,它实现了从物理层到网络层 的封装分用**,**组建局域网,进行网络数据包的转发。
对于一台交换机 ,它实现了从物理层到数据链路层 的封装分用**,** 对路由器接口的拓展**。**
2.9 网络数据通信的基本流程
我通过聊天软件发送 hello给另一个人。
2.9.1 封装
以下的过程,数据从上层到下层的过程,每一步数据都要进一步加工,这样的过程叫做封装。
2.9.2 应用程序获取到用户的输入,然后构造出来一个应用层的数据包。
这个数据包通常是结构化的数据(包含很多属性),网络传输的数据本质就是"字符串"或者"二进制的bit流"。
发送数据时候把这个结构化的数据 转换为 字符串或者二进制bit流的过程,叫序列化。
把字符串或者二进制bit流 转换为 结构化的数据的过程,叫反序列化。
应用层调用传输层提供的API,将数据包发送给传输层。
2.9.3 传输层拿到数据后,构造出来一个传输层的数据段。
传输层的主要协议是**:TCP协议和UDP协议**,下面我以TCP协议来解释:
传输层数据段 = TCP数据段 = TCP段头 + TCP载荷,TCP段头包括一些TCP的属性,例如:源端口和目的端口,应用层使用的协议,TCP载荷就是应用层数据包。
传输层构造好数据后,调用网络层提供的API,将数据段发送给网路层。
2.9.4 网络层接收到数据后,构造出来一个网络层的数据报。
网络层的主要协议是:IP协议。
网路层数据报 = IP数据报 = IP报头 + IP载荷,IP报头包含IP的一些属性,例如:源IP,目的IP,传输层使用的协议,IP载荷就是传输层的数据段。
网络层构造数据后,调用数据链路层提供的API,将数据报发送给数据链路层。
2.9.5 数据链路层接收到数据后,构造出来一个数据链路层的数据帧
数据链路层的主要协议是:以太网。
数据链路层数据帧 = 以太网数据帧 = 以太网帧头 + 以太网载荷 + 以太网帧尾,以太网数据帧里面存储着网络层使用的协议。
数据链路层构造好数据后,将数据交给硬件设备(网卡)。
2.9.6 网卡将这些数据传输出去
网卡通过光信号/电信号/电磁波信号将数据传输出去。
2.9.7 分用
此时数据到达接收端,数据在接收端从下层到上层逐渐解析数据的过程叫做分用。
2.9.8 数据到达接收方的网卡
接收方的网卡将这些光电信号转换为二进制的数据,这里数据传输的方式和数据链路层相关联,
如果数据通过网线/光纤传输过来,此时数据链路层就会使用以太网协议。
如果数据通过wifi传输过来,此时数据链路层就会使用802.11协议。
网卡将这些解析后的二进制数据交给数据链路层。
2.9.9 数据链路层接收到二进制数据
此时数据链路层会按照对应协议(以太网协议)对这些二进制数据进行解析,把以太网数据帧中的以太网帧头和帧尾去掉,而以太网的帧头中存着网络层使用的协议。
此时数据链路层把解析好的数据(以太网载荷)交给网络层。
2.9.10 网络层接收到以太网载荷数据
此时网络层会把这个数据按照(IP协议)进行解析,把网络层数据报去除报头,报头里面存储有传输层使用的协议。
网络层把解析后的数据(IP载荷)交给传输层。
2.9.11 传输层接收到IP载荷数据
传输层会把这个数据按照(TCP协议)进行解析,传输层数据段去除段头,段头里面存在目的端口号,告诉我们是那个应用程序接收数据。
传输层把解析的数据(TCP载荷)交给应用层。
2.9.12 应用层接收到TCP载荷数据
应用层会按照不同的协议来解析这个数据包,然后把数据包里面的重要内容显示到屏幕上。
3. 网络编程
3.1 什么是网络编程
网络编程是指网络上的主机,通过不同的进程,用编程的方式实现网络通信。
网络编程只需要是不同进程之间进行通信就行,可以是一台主机上的不同进程之间通过网络来通信。
3.2 网络编程的基本概念
发送端:就是数据的发送方进程,发送端的主机就是网络通信中的源主机。
接收端:就是数据的接收方进程,接受端的主机就是网络通信的目的主机。
收发端:也就是发送端和接收端两端。
这里的发送端和接收端是相对而言的,并不是说那个主机一定就是某个端。
**请求和响应:**获取一个网络数据,一般要经过两次网络数据传输:
第一次是:请求数据的发送,发送端到接收端。
第二次是:响应数据的发送,接收端到发送端。
**客户端和服务端:**在网络数据传输的过程中,把提供服务的一端称为服务端,把获取服务的一端称为客户端。
4. 套接字(Socket)
套接字是操作系统提供的进行网络通信的接口,让应用程序能够通过网络发送数据/接收数据,也就是传输层给应用层提供的API。
传输层的协议有TCP协议和UDP协议,因此操作系统提供了两套基于不同协议的API,供我们进行网络编程,基于Socket套接字的网络程序开发就是网络编程。
**流套接字:**传输层使用了TCP协议,该协议的特点:有连接,可靠传输,面向字节流,全双工。
**数据报套接字:**传输层使用了UDP协议,该协议的特点:无连接,不可靠传输,面向数据报,半双工。
有连接VS无连接:
这里的连接不是物理形式的连接,而是指进行网络通信时候,发送请求和接收请求的两端知道对方的IP地址和端口号等信息,这叫做有连接。
可靠传输VS不可靠传输:
数据在进行网络通信时候,会存在数据丢失的情况(丢包),而可靠传输不是说数据100%传输过去,当接收端收到数据后会返回一个确认(ACK)给发送端,如果发送端没有收到确认就会确认数据出现丢包情况。而不可靠传输是直接把数据发送出去后就不管了。
面向字节流VS面向数据报:
面向字节流:是在读写数据的过程,以字节为单位。
面向数据报:在读写数据过程中,以一个数据报为单位。
全双工VS半双工:
全双工:一个通信链路支持双向通信,既能读又能写。
半双工:一个通信链路支持单向通信,要么读要么写。
5. UDP数据报套接字编程
我们直接操作网卡这样的硬件设施不太好操作,操作系统管理硬件设施时候把硬件设施抽象成文件进行管理的,而网卡就被抽象成Socket这样的文件进行管理的,我们可以通过操作这个文件来实现网络编程。
5.1 DatagramSocket类
该类是用来发送和接收DatagramPacket(数据报)的。
构造方法:
DatagramSocket(),创建一个套接字(端点),绑定到本机的一个随机端口,一般用于客户端。
DatagramSocket(int port),创建一个套接字(端点),绑定到本机的一个指定端口,一般用于服务端。
普通方法:
void receive(DatagramPacket p),从此套接字接收数据报,没有接收到数据报就会阻塞等待。
void send(DatagramPacket p),从此套接字发送数据报,不会阻塞等待,直接发送。
void close(),关闭此数据报套接字,释放PCB描述符表中的资源。
5.2 DatagramPacket类
该类相当于数据报,将要发送的数据,源地址或者目的地址封装起来,是数据传输的载体。
构造方法:
DatagramPacket(byte[] buf,int length),构造一个DatagramPacket来接收数据报,将接收的数据保存在字节数组中,指定接收的长度。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address),构造一个 DatagramPacket作为数据报发送,发送的数据为字节数组,从0到指定长度的数据,address是目的主机的IP和端口号。
普通方法:
InetAddress getAddress():从接收的数据报中获取发送端的IP地址,或者从发送的数据报中获取接收端的IP地址。
int getPort():从接收的数据报中获取发送端的端口号,或者从发送的数据报中获取接收端的端口号。
byte[] getData():获取数据报中的数据。
5.3 InetSocketAddress类
在构造数据报时候,参数需要传入SocketAddress类型参数,此时可以使用InetSocketAddress类来创建,InetSocketAddreaa类继承SocketAddress抽象类。
这个普通类里面的构造方法:
InetSocketAddress(InetAddress addr,int port),该方法创建一个Socket对象,包含IP地址和端口号。
5.4 代码实现
回显服务器就是客户端发送数据给服务器,服务器不做处理,直接将数据返回给客户端。
下面实现的是回显服务器的代码:
java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//接收或者发送数据报
private DatagramSocket socket = null;
//指定一个端口号让服务器来使用
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
//启动器
System.out.println("服务器启动");
while(true) {
//每循环一次,相当于处理一次客户端的请求
//处理请求的步骤
//1. 读取请求并解析
//requestPacket是UDP的数据报,里面传入的字节数组用来存储UDP的载荷部分。
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//接收数据报
socket.receive(requestPacket);
//把读取到的二进制数据(字节数组)转换成字符串,只用转换有效的部分。
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2. 根据请求,计算响应。
//这里使用的是回显服务器,所以直接将请求作为响应返回
String response = process(request);
//3. 把响应返回给客户端
//根据响应构造 DatagramPacket,返回给客户端
//这里将字符串转为二进制数据(字节数组),这里直接通过getSocketAddress()方法来获得目的IP和目的端口
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
request.getBytes().length,requestPacket.getSocketAddress());
//此处不能直接发送,因为UDP没有保存目的IP和目的端口,需要我们指定
socket.send(responsePacket);
//4. 打印一个日志
//打印 源IP,源端口号,请求内容,回响内容
System.out.printf("[%s:%d],request: %s, response: %s",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
//启动服务器
server.start();
}
}
下面是客户端的代码:
java
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
//用来发送数据报的
private DatagramSocket socket = null;
//UDP本身不存储服务器的目的IP的目的端口
private String serverIp;
private int serverPort;
//此处的构造方法要指定访问的服务器的地址
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
//客户端使用随机端口号
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true) {
//1. 从控制台读取用户输入的内容
System.out.println("请输入要发送的内容:");
//判断发送的内容是否为空
if (!sc.hasNext()) {
break;
}
String request = sc.next();
//2. 把请求发送给服务器,此时要构造DatagramPacket对象
//不仅要构造载荷,还要设置服务器的IP和端口
DatagramPacket requestSocket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
//3. 发送数据报
socket.send(requestSocket);
//4. 接收服务器的响应
DatagramPacket responseSocket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responseSocket);
//5. 将服务器读取到的数据进行解析,打印出来
String response = new String(responseSocket.getData(), 0, responseSocket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
6. 基于UDP协议实现一个简单翻译服务器
我们只需要把上面的服务器获取的请求,计算出对应的响应,代码进行修改。
java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 87111
* Date: 2025-11-18
* Time: 19:20
*/
public class UdpEchoServer {
//接收数据报
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//服务器指定一个固定端口
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
//客户端发送很多请求
while(true) {
//每循环一次相当于一次请求
//处理请求步骤
//1.接受请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[5096],5096);
//接受客户端发来的请求,放到requestPacket里面
socket.receive(requestPacket);
//将请求中的二进制数据解析成字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求,计算响应
String response = process(request);
//3.将请求发送给客户端
//将响应构造成数据报
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,
response.getBytes().length,requestPacket.getSocketAddress());
//发送响应
socket.send(responsePacket);
//4.打印一个日志
System.out.printf("[%s:%d]--request: %s, response: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//实现中英翻译
private String process(String request) {
HashMap<String,String> dict = new HashMap<>();
//初始化字典
dict.put("小猫", "cat");
dict.put("小狗", "dog");
dict.put("小兔子", "rabbit");
dict.put("小鸭子", "duck");
return dict.getOrDefault(request,"未找到该翻译");
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
7. TCP流套接字编程
7.1 ServerSocket类
ServerSocket类是用来创建TCP服务端套接字。
构造方法:
ServerSocket(int port),创建一个服务端的流套接字,并且绑定指定的端口号。
普通方法:
Socket accept(),开始监听服务端绑定的端口,当有客户端与该端口建立连接后就返回一个Socket对象,基于这个对象和客户端建立连接。
void close(),关闭套接字。
7.2 Socket类
该类是用来创建客户端的套接字,主动给服务端发送连接请求,和服务端建立连接后,与服务器的socket通过输入流和输出流进行交换数据。
构造方法:
Socket(String host,int port),通过服务端的IP和端口号发起连接。
普通方法:
InetAddress getInetAddress(),返回套接字连接的服务端的IP地址。
InputStream getInputStream(),返回套接字输入流。
OutputStream getOutputStream(),返回套接字输出流。
7.3 代码实现
客户端的代码:
java
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 {
private Socket socket = null;
//通过构造方法传入要连接的服务器
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//直接把服务器的IP和端口号作为参数传进来
//该对象直接在底层和服务端建立连接,保存了对端的信息
socket = new Socket(serverIP,serverPort);
}
public void start() {
Scanner sc = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//为了使用方便进行套壳处理
//接收响应
Scanner scanner = new Scanner(inputStream);
//发送请求
PrintWriter writer = new PrintWriter(outputStream);
//从控制台读取请求,发送给服务器
while(true) {
System.out.println("请输入发送的内容:");
//1.从控制台读取请求
String request = sc.next();
//2.将请求发送给服务器
//这里的发送请求给服务器,是先将请求放在"发送缓冲区"(内存里),并没有真正发送给网卡,而是累积到一定程度后一起发送。
//这里是自动加上换行的,如果不加换行,服务器端在读取时候就会阻塞,没有读到空白字符。
//TCP中默认按照空白符分开每个请求
writer.println(request);
//刷新缓冲区,真正发送数据
writer.flush();
//3.读取服务器返回的响应
String response = scanner.next();
//4.将响应打印出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
服务器的代码:
java
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class TcpEchoServer {
private ServerSocket socket = null;
//构造对象,并绑定端口号
public TcpEchoServer(int port) throws IOException {
//设置服务器端口号
socket = new ServerSocket(port);
}
public void start() throws IOException {
//启动服务器
System.out.println("启动服务器");
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
while(true) {
//TCP需要先处理客户端发来的连接
//如果没有连接,就会阻塞等待客户端的连接
//如果连接成功,就通过clientSocket来和客户端进行通信
Socket clientSocket = socket.accept();
//有可能多个客户端访问当前服务器
//方法一:此时可以每次跟一个客户端建立连接后,创建一个线程处理客户端的请求
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
//创建线程和销毁线程也会消耗资源。
// 方法二:使用线程池的方法来解决多个客户端访问服务器的情况
executorService.submit(() -> {
processConnection(clientSocket);
});
}
}
//处理一个客户端的连接
//有可能会有多个客户端进行请求和响应
public void processConnection(Socket clientSocket) {
System.out.println("客户端上线了");
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//针对输入流和输出流又套了一层
//接收请求
Scanner sc = new Scanner(inputStream);
//发送响应
PrintWriter writer = new PrintWriter(outputStream);
while(true) {
//1. 读取请求,并解析
if(!sc.hasNext()) {
System.out.println("没有请求");
}
String request = sc.next();
//2.根据请求,计算响应
String response = process(request);
//3.将响应发送给客户端
writer.println(response);
//刷新缓冲区
writer.flush();
//打印日志
System.out.printf("[%s,%d] request: %s response: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
//这里如果完成通信后,要及时关闭socket建立的连接
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}