指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)
①发送端:在一次网络数据传输时,数据的发送方进程,称为发送端
②接收端:在一次网络数据传输时,数据的接收方进程,称为接收端
③收发端:在一次网络数据传输时,发送端和接收端两端,也简称为收发端
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送
举例:
好比人在快餐店点一份炒面
先要有人发起请求:点一份炒面
再有快餐店提供的对应响应:提供一份炒面
①客户端:发送请求和获取服务的一方进程,称为客户端
②服务端:在常见的网络数据传输场景下,把处理请求和提供服务的一方进程,称为服务端 (服务端即对外提供服务)
③常见的场景:
1. 客户端先发送请求到服务端
2. 服务端根据请求数据,执行相应的业务处理
3. 服务端返回响应:发送业务处理结果
4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
我们要想实现网络编程,写一个应用程序,主要靠的是调用传输层提供的API接口
而传输层最主要的协议是UDP和TCP,因此,靠的主要就是UDP和TCP提供的API接口
Socket套接字主要针对传输层协议划分为UDP和TCP
UDP的API主要提供两个类:DatagramSocket和DatagramPacket
TCP的API主要提供两个类:ServerSocket和Socket
Socket套接字是由系统提供用于网络通信的技术
Socket套接字是基于TCP/IP协议的网络通信的基本操作单元
基于Socket套接字的网络程序开发就是网络编程
①有连接:客户端和服务器之间,利用内存保存对方一端的信息,当双方都保存这个信息之后,就能建立连接了,对于TCP而言,必须要建立连接才能使用
举例:就像打电话一样,对方不接通,你们是无法通信的
②可靠传输:A给B发的消息不是100%能发到的,但是A会尽可能的把消息传给B,假设传输失败,A也能感知到失败;假设传输成功,A也能知道自己传输成功了
举例:就像打电话一样,当你说了话但是对方没有响应时,你就会知道此时信号有问题
(可靠传输会使传输效率降低)
③面向字节流:读写的基本单位是字节,类似于文件操作
④全双工:一个通道,可以双向通信
举例:类似于车道
⑤无大小限制
(比UDP好的地方,传输数据大小没有限制)
⑥数据传输过程出错,即发送方和接收方的校验和不同,会重新发送
①无连接:客户端和服务器之间,利用内存保存对方一端的信息,当双方都保存这个信息之后,就能建立连接了,对于UDP而言,无须建立连接也能使用
举例:就像发微信一样,有他的好友就行,发消息不用经过同意
②不可靠传输:A给B发的消息不是100%能发到的,但是A会尽可能的把消息传给B,假设传输失败,A无法感知到失败
举例:就像发微信一样,当你发了一条消息但是对方没有响应,可能是已读不回,但你却不知道是没看到还是故意不回
(不可靠传输会使传输效率提高)
③面向数据报:读写的基本单位是数据报
④全双工:一个通道,可以双向通信
举例:类似于车道
⑤大小受限:UDP协议首部中有一个16位的最大长度
也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
(使用UDP的DatagramPacket时给字节数组初始化的时候值不能超过64K)
-------------------------------------------------------------------------------------------------------------------------
(UDP缺陷:无法表示一个较大的数据段)
当我们要传输的数据长度大于2个字节的长度时:
①方法一:我们可以在应用层进行分包(拆成多个部分),然后再通过多个UDP数据段分别发送。接收方收到后,再把几个包重新拼接成完整的数据;一般不推荐,比较麻烦
②方法二:不适用 UDP协议了,改成 TCP协议!!!因为 TCP 中没有这样的长度限制
⑥数据传输过程出错,即发送方和接收方的校验和不同,直接丢弃,不会重新发送
在写网络编程的时候,UDP和TCP都需要抛出SocketException异常同时SocketException也属于IO异常
DatagramSocket用于发送和接收UDP数据报
注意:DatagramSocket其实是一个Socket对象
我们需要知道,操作系统使用文件这样的概念,是为了管理一些软硬件资源,而操作系统也是用文件这样的方式去管理网卡,而表示网卡的这类文件,我们就称为Socket文件
注意:Java的这些Socket对象就对应系统里的这些Socket文件,也就是对应着网卡这类的文件!因此,要进行网络通信,就必须要有Socket对象!
①DatagramSocket() :创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口
(一般用于客户端,客户端使用哪个端口,由系统来自动分配)
②DatagramSocket(int port): 创建一个UDP数据报套接字的Socket,绑定到本机指定的port端口
(一般用于服务端,服务端使用哪个端口,由自己手动分配)
问题:为什么客户端要用无参构造方法,服务端要用带参数port的构造方法???
解答:我们都知道端口号是用来区分不同程序的;对于服务器来说,要有一个固定的端口号,这样当客户端来找的时候就很方便。比如说我在学校饭堂专门卖炒饭,位于饭堂的7号窗口,我是固定不动的,我就相当于服务端,为同学们提供服务也就是提供炒饭,当同学们想吃炒饭,直接来7号窗口即可,方便又快捷;假设有个A同学来吃的时候,A同学去7号窗口拿完炒饭然后找个位置坐,A同学就相当于客户端,A同学想要获取炒饭也就是获取服务嘛,那么A同学找的位置就相当于客户端的端口,但是,A同学每次来的时候都能坐之前的那个位置吗?那个位置可能被别人坐了也不一定吧?因此,客户端的端口是没办法固定的
一个客户端的主机上面运行程序会很多,假设你手动指定端口号,那万一这个端口号被别的程序占用了呢?一个端口只能被一个进程占用,所以让系统自动分配客户端端口才是一个明智的选择!
①void receive(DatagramPacket p):接收数据报
这里的参数类型是DatagramPacket
这个参数DatagramPacket p是一个输出型参数,传入receive的是一个空对象,receive就会把这个空的对象的内容给填充上,当receive执行结束,于是就得到了一个装满内容的DatagramPacket
(如果客户端没有发送请求,也就是没有接收到数据报时,方法会阻塞等待)
②void send(DatagramPacket p):发送数据报
这里的参数类型是DatagramPacket
(不会阻塞等待,直接发送)
③void close():关闭此数据报套接字
当整个程序只有一个Socket对象时,不使用close关闭也没事,因为此时这个对象的生命周期很长,不是频繁创建的,会跟随着程序的关闭而结束
当整个程序有多个Socket对象时,必须使用close,因为此时这个对象的生命周期很短,是频繁创建的
DatagramPacket表示了一个UDP的数据报,也是用来发送和接收UDP数据报
①DatagramPacket(byte[] buf, int length)
构造一个DatagramPacket以用来接收数据报
接收的数据保存在字节数组(第一个参数buf) (注意:这里的数组不能超过64k)
接收指定长度(第二个参数 length)
②DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):
构造一个DatagramPacket以用来发送数据报
发送的数据为字节数组(第一个参数buf) (注意:这里的数组不能超过64k)
从offset到指定长度length
address指定目的主机的IP和端口号(构造UDP发送的数据报时,需要传入SocketAddress类的参数)
DatagramPacket这个对象是用来保存数据的内存空间,但它跟我们以往学的集合类不一样,以往的集合类是自动创建空间来保存,而DatagramPacket需要手动指定内存空间大小
DatagramPacket这个对象里就包含了通信双方的IP和端口号
(一般通过getSocketAddress()方法即可获取IP和端口号)
DatagramPacket的构造方法创建好对象后,接收/发送数据都需要传给DatagramSocket的普通方法;DatagramPacket只是作为DatagramSocket的方法参数
DatagramSocket 是取外卖和送外卖的外卖小哥, 而 DatagramPacket就是外卖
先有外卖,再把外卖给小哥;即有了DatagramPacket对象,再把它交给DatagramSocket
由外卖小哥即DatagramSocket负责去送外卖,即传输数据
①InetAddress getAddress():
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
(获取对方的IP地址)
(假设在服务器中,它接收客户端的请求,即接收客户端的数据报,通过getAddress就可获得客户端的IP)
②int getPort():
从接收的数据报中,获取发送端主机端口号;或从发送的数据报中,获取接收端主机端口号
(获取对方的端口号)
③byte[] getData():获取数据报中的数据
(返回值是实际数据大小的字节数组)
④int getLength():获取数据报中的实际长度的数据
⑤SocketAddress getSocketAddress():
获取要将此包发送到的或发出此数据报的远程主机的SocketAddress(通常为IP地址+端口号)
使用DatagramPacket API的发送数据报构造方法时,需要传入 SocketAddress
①addr:IP地址
②port:端口号
InetSocketAddress是SocketAddress的子类
①InetAddress.getLocalHost():获取本机的InetAddress对象
②InetAddress.getByName(指定主机名):根据指定主机名IP获取InetAddress对象
(一般用于客户端发出请求时需要获取带有服务器IP的InetAddress对象)
③InetAddress.getByName(指定域名):根据域名返回InetAddress对象
(1)联系
InetAddress和InetSocketAddress都是SocketAddress的子类
(2)区别
①InetAddress封装了计算机的ip地址和DNS,没有端口
②InetSocketAddress封装了计算机的ip地址和DNS,包括了端口
客户端发啥,服务器回啥
客户端:
1.输入请求,将请求发送给服务器
2.接收服务器发送回来的响应
(先发后收)
服务器:1. 接收并读取客户端发来的请求, 然后解析
2. 根据请求, 计算出响应(重点在于如何第二步计算)
3.把响应写回给客户端
(先收后发)
运行顺序:一定是服务器先启动,然后等待客户端启动和发送请求,顺序不能乱
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
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) {
// 使用while循环反复的, 长期的执行针对客户端请求处理的逻辑.
// 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.
// 1. 接收和读取客户端发来的请求, 并解析
// 接收的数据保存在byte[4096]数组,一次读取4096个字节长度
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
// 将保存数据的对象requestPacket传给DatagramSocket的receive方法作为参数,用来接收数据报
socket.receive(requestPacket);
// 这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.
// 通过String构造方法将客户端发来的requestPacket它里面存储的是字节数组,将字节数组转为字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算出响应
// 下面我们自定义个porcess方法,用来计算
// 响应的请求放到response
String response = process(request);
// 3. 把响应写回给客户端,即服务器发送数据报给客户端
// 此时需要告知网卡, 要发的内容是啥, 要发给谁.
// 这里我们使用getBytes()方法将字符串转换为字节数组
// 为毛要转为字节数组??DatagramPacket构造方法参数类型要求的就是字节数组
// 步骤一中的 requestPacket接收客户端发来的数据时就包括了客户端的IP和端口
// 通过requestPacket.getSocketAddress得知客户端的IP和端口,此时就知道要发送给谁了
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
// 记录日志, 方便观察程序执行效果.
// requestPacket.getAddress().toString()获取客户端的IP并打印
// requestPacket.getPort()获取客户端的端口并打印
System.out.printf("[%s:%d] req: %s, resp: %s\n", 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); //指定服务器的固定端口是9090
server.start();
}
}
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
// 这里的serverIp和serverPort其实是服务器的 ip 和 服务器的端口
// 客户端只有连接上同一个端口即同一个程序才能获取服务器的服务
serverIp = ip;
serverPort = port;
// 这个 new 操作, 因为现在我们写的是客户端,因此就不再指定端口了. 让系统自动分配一个空闲端口
socket = new DatagramSocket();
}
// 让这个客户端反复的从控制台读取用户输入的内容. 把这个内容构造成 UDP 请求, 发给服务器. 再读取服务器返回的 UDP 响应
// 最终再显示在客户端的屏幕上.
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
while (true) {
// 1. 从控制台读取用户输入的内容
System.out.print("-> "); // ->是一个命令提示符, 提示用户要输入字符串.
String request = scanner.next(); //用户输入请求
// 2. 构造请求对象, 并发给服务器.
// 因为用户输入的是字符串,传输的时候要转为字节数组,这里我们通过getBytes
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 读取服务器的响应, 并解析出响应内容.
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 将服务器回应的内容转换成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 显示到屏幕上.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
//接下来客户端就访问这个9090的服务器端口,与9090端口的服务器进行通信
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
先启动服务器,再启动客户端
通过IDEA还可以开启多个客户端
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
//定义一个HashMap获取对应的翻译
private Map dict = new HashMap<>();
//直接使用继承的方式,避免了重复写代码
//因此start方法就不要写了,直接就复用了之前的 start !
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("communicate", "沟通;交流");
dict.put("poole", "普尔");
dict.put("dump", "丢弃;垃圾场");
dict.put("dumb","哑的;无法讲话的");
dict.put("drum","打鼓");
}
// 调整process方法
@Override
public String process(String request) {
// 把请求对应单词的翻译, 给返回回去
// 查不到就返回default值
return dict.getOrDefault(request, "该词没有查询到!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
服务器端通过创建 ServerSocket 对象实现
(只用于服务器)
ServerSocket(int port):创建一个服务端流套接字Socket,并绑定到指定端口
①Socket accept()
监听构造方法中指定的端口,当接收到客户端的连接请求后,会生成一个 Socket 对象与客户端连接,并返回此连接对应的 Socket 对象(可以看到accept方法的返回类型是Socket),我们一般会写一个Socket对象来接收,接下来就通过我们写的这个Socket对象来与客户端进行通信;反之,如果没有收到客户端的连接请求,就会阻塞等待
(这就是TCP与UDP不同的地方,前面我们提到过TCP的特点就是必须建立连接才能使用)
②void close():关闭此套接字
当整个程序只有一个Socket对象时,不使用close关闭也没事,因为此时这个对象的生命周期很长,不是频繁创建的,会跟随着程序的关闭而结束
当整个程序有多个Socket对象时,必须使用close,因为此时这个对象的生命周期很短,是频繁创建的
它可以让客户端和服务器之间相互通信
(既可用于客户端,也可用于服务器)
(一般是用来创建客户端对象、建立连接)
①Socket可以是客户端的Socket
②Socket也可以是服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket,上面的accept方法就提到了
不管是客户端还是服务端的Socket,都是双方建立连接以后,保存的对端信息
(例如:服务端的Socket保存着客户端的IP和端口)
Socket(String host, int port)
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
(创建一个客户端,系统随机分配客户端端口,然后与对应参数IP和端口的服务器连接)
注:这里的IP和port都是服务器的ip和端口,因为客户端要发信息肯定得先知道服务器的位置
①InetAddress getInetAddress():返回套接字所连接的IP地址
②int getPort():返回套接字所连接的端口
③InputStream getInputStream():返回此套接字的输入流
④OutputStream getOutputStream():返回此套接字的输出流
⭐1.Socket 对象内部包含了两个字节流对象, 可以把这两个字节流对象通过get方法(即getInputStream和getOutputStream)获取到, 完成后续的读写工作
⭐2.此时就不用new这两个字节流对象,而是直接get方法,于是就能够获取Socket对象中的读和写,就像耳机和麦克风一样,读就用耳机InputStream,写就用麦克风OutputStream
3.对于服务器来说,即读请求和写响应,随后就可直接在后面的读取请求中调用InputStream的相关方法和写回响应中调用OutputStream的相关方法
4.对于客户端来说,即写请求和读响应,随后就可直接在后面的写入请求中调用OutputStream的相关方法和读取响应中调用InputStream的相关方法就行
5.TCP主要是面对字节流,因此靠的是字节流的两个读写类
而UDP面对的是数据报,因此可通过专门的构造方法填写数据报,发送数据报,解析数据报
1.Socket 是用于建立连接的类,它可以让客户端和服务器之间相互通信
2.ServerSocket 是用于监听连接请求的类,它全是应用在服务器,它在服务器端等待客户端的连接请求,并在连接成功后与客户端建立对应的 Socket 连接
3.当客户端与服务器建立连接时
①客户端通过创建 Socket 对象实现,目的在于建立客户端后连接服务器,主要是连接作用
②服务器端则通过创建 ServerSocket 对象实现,目的在于建立指定端口的服务器
4.ServerSocket是Socket的子类
客户端:
1.输入请求,将请求发送给服务器
2.接收服务器发送回来的响应
(写请求读响应)
服务器:与UDP不同的是,这里需要先建立连接
1.建立与客户端的连接
2. 接收并读取客户端发来的请求, 然后解析
3. 根据请求, 计算出响应(重点在于如何第二步计算)
4.把响应写回给客户端
(读请求写响应)
运行顺序:一定是服务器先启动,然后等待客户端启动和发送请求,顺序不能乱
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;
public class TcpEchoServer {
//通过ServerSocket来创建服务端的对象
private ServerSocket serverSocket = null;
// 因为考虑到客户端的请求可能有多个,线程资源开销比较大,可以使用线程池
// 此处不应该创建固定线程数目的线程池,因为如果固定了线程,那就相当处理客户端的请求给了限制
// 因此我们使用newCachedThreadPool来创建一个动态线程数目的线程池
private ExecutorService service = Executors.newCachedThreadPool();
// 通过构造方法初始化的方式,这个new操作就会使服务器绑定指定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//使用while循环,接收多个客户端
while (true) {
//调用serverSocket.accept就可以监听客户端是否发出请求
//如果发出请求,accept就会返回一个Socket对象
//在这里我们创建一个Socket对象即clientSocket来接收accept返回的Socket对象
//往后我们就通过clientSocket来与客户端进行通信
Socket clientSocket = serverSocket.accept();
// 使用多线程的原因在于,如果是单个线程,假设已经有一个客户端发来请求了,clientSocket对象只能处理这一个请求
// 简单来说,就是不能使用多个客户端来发送请求,只能等一个请求解决完了才能接着下一个请求
// 使用多线程后,accept就通过主线程不断接收客户端请求,即每次有一个客户端,就都创建一个新线程去服务
// 这里我们不使用原始的线程创建方式,而是使用线程池, 这样就可以解决上述资源频繁创建删除问题
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
//通过调用submit方法来执行任务
service.submit(new Runnable() {
@Override
public void run() {
//通过processConnection这个方法完成服务器的一系列操作
//根据参数clientSocket来与客户端通信
processConnection(clientSocket);
}
});
}
}
// 通过这个processConnection方法来处理一个连接的逻辑
private void processConnection(Socket clientSocket) {
//因为有了返回的clientSocket,证明已经和客户端连接上了,这里我们打印一下客户端的相关信息
//打印一下客户端的IP和端口
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就可以依次从①读取请求, ②根据请求计算响应, ③返回响应这三步走了.
// Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
// 这里的Socket对象即clientSocket就包括了客户端发来的请求,我们可以通过InputStream读取
// 使用try with resource自动close关闭资源
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 一次连接中, 可能会涉及到多次请求/响应,因此这里我们使用while循环
while (true) {
// 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner,通过Scanner直接读取客户端发来的请求
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 如果读到空白符,返回false
// 读取完毕, 客户端下线.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
// 因为next()只能读取空格前的数据
// scanner.next()方法读取客户端请求
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 把 OutputStream 使用 PrintWriter 包裹一下, 方便进行发数据.
// PrintWriter就可以直接从字符串角度写入,而不用强制转换成字节数组
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端.
// 此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
writer.println(response);
// 这里还需要加一个 "刷新缓冲区" 操作.
writer.flush();
// 日志, 打印当前的请求详情.
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
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(9090);
server.start();
}
}
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 {
//通过Socket来创建客户端的对象
private Socket socket = null;
// 要和服务器通信, 就需要先知道, 服务器所在的位置
// 这里的serverIp和serverPort都是服务器的IP和端口
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 这个 new 操作,使此时的socket对象与服务器对应IP的主机和对应端口的进程建立连接
// 连接完成之后, 就完成了 tcp 连接的建立
// 此时服务器的accpet方法就会接收到感应
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
//这里是客户端输入请求的scannerConsole
Scanner scannerConsole = new Scanner(System.in);
//使用try with resource自动close关闭资源
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台输入字符串.
System.out.print("-> ");
//客户端输入请求后用request保存
String request = scannerConsole.next();
// 2. 把请求request发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
// 使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
printWriter.println(request);
// 不要忘记 flush, 确保数据是真的发送出去了!!
printWriter.flush();
// 3. 从服务器读取响应.
//这里的scannerNetwork是读取响应的Scanner对象
Scanner scannerNetwork = new Scanner(inputStream);
//通过next()读取响应,并用response保存
String response = scannerNetwork.next();
// 4. 把响应打印出来
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//客户端通过连接服务器的指定端口9090
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}