本篇通过了解套接字,TCP与UDP协议的特点,使用UDP的api套接字与TCP的api套接字进行回显程序的网络通信,如有错误,请在评论区指正,让我们一起交流,共同进步!
1.1 认识套接字
套接字:应用层调用传输层,操作系统内核给应用层提供一组用来网络编程的api,这就称为 socket api = 套接字api;
针对TCP与UDP协议,这里主要认识关于UDP的api 与 TCP的api 来用于网络通信;
认识api先认识 TCP 与 UDP 的特点吧!
1.2 TCP 与 UDP的主要特点
UDP:
① 无连接:使用UDP通信,双方不需要保存对端(客户端与服务器)的信息;
例如:发短信,不需要记录对方的电话,对方也不需要记录你的电话 (不用建立连接),直接根据电话号直接发送信息;
② 不可靠传输:一方发送消息,不需要知道另一方是否接收到了信息;
③ 面向数据报:以一个UDP数据报为基本单位读写数据;- 有限制;
④ 全双工:一条路径双向通信;
【注】有无连接:通信双方是否记录对方的信息;
有连接:通信双方需要记录对方的信息;
无连接:通信双方不需要记录对方的信息;
TCP:
① 有连接:使用TCP通信,双方需要保存对端的信息;
例如:打电话,一方打电话,另一方需要接通电话 (建立连接),双方才能通信;
② 可靠传输:一方发送信息,尽量保证信息发送到了另一方(但也不能保证一定成功,只是尽全力);
③ 面向字节流:以一个字节为传输基本单位 - 读写数据比较灵活
④ 全双工:双向通信;
【注】半双工:在一条路径上,A方向B方发送消息,B只能等待A发完消息后才能向A发送信息;
全双工:A向B发送消息的同时,B也可以向A发送消息;
认识socket对象:
socket 是系统中一个特殊的文件进行网络通信,需要socket文件对象,通过socket文件对象,间接操作网卡;
Datagram: 数据报;Socket: socket对象;
【注】socket对象可以被客户端 与 服务器使用;服务器使用socket需要指定关联一个端口号 - 端口号不变才能方便客户端找到服务器;客户端使用socket不需要手动指定,系统会自动分配空闲的端口号;
DatagramPacket: udp数据报对象;
回显程序:自己给自己发送并接收信息;
服务器的核心工作:
① 读取客户端请求并解析
② 根据请求计算响应
③ 把响应写回到客户端
首先构造socket对象,从而间接操作网卡进行读取;
再构造带有一个参数的构造方法,给服务器分配端口;
private DatagramSocket socket = null;
//绑定端口号不一定会成功;- 端口号可能被别的进程占用
//同一主机,同一时刻,一个端口只能被一个进程所占用
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
服务器主要逻辑过程:
① 读取客户端请求:使用receive()方法, 需要构造requestPacket数据报把数据读取到数据报中;
② 知道客户端输入的是字符,为方便操作将数据报(字节数组)构造为字符串;
③ 根据请求计算具体响应 - 根据具体情况修改;
④ 将响应发送回客户端,根据响应是字符串,构造数据报此时需要指定客户端的IP和端口;
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器执行不可能只执行一个请求,需要执行多个请求就用到了循环
while (true) {
//1.读取客户端请求
DatagramPacket requestPacket = new DatagramPacket(new byte[5000],5000);
//receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;
// -》从网卡读取内容,写的数据报中;
socket.receive(requestPacket);//服务器先启动,如果此时没有客户端请求,就会阻塞等待,等待客户端发送数据过来;
//解析 -> 此处为了方便后续操作,拿字节数组构造成字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//getLength : 获取真实长度;
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端 - 写回网卡
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).length,
//requestPacket是客户端得到的,getSocketAddress得到的就是客户端的IP和端口
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) {
return request;
}
【注】receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;
服务器总代码:
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[5000],5000);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端 - 写回网卡
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).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) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
首先构造socket对象,方便操作读取网卡;
再构造带两个参数的构造方法:指定服务器的IP和端口,让客户端容易找到服务器;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//客户端启动需要知道服务器的位置,根据服务器的IP和端口
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//客户端不用指定端口,会自动分配空闲端口;
socket = new DatagramSocket();
//为了方便之后使用
this.serverIP = serverIP;
this.serverPort = serverPort;
}
}
客户端执行逻辑:
① 等待客户端输入请求;
② 把请求发送给服务器,需构造数据报并指定服务器IP和端口;
③ 客户端接收响应,构造数据报使用receive()接收;
④ 打印响应结果,再将数据报转换为字符串
代码如下:
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
//多次交互
while (true) {
System.out.print(" -> ");
//1.等待控制台输入
String request = scanner.next();
//2.把请求发送到服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
//通过静态发送InetAddress设置IP地址;
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.客户端接收响应
DatagramPacket responsePacket = new DatagramPacket(new byte[5000],5000);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.打印日志
System.out.printf("request: %s, response: %s\n", request,response);
}
}
【注】构造数据报DatagramPacket requestPacket:请求的起始0位置和请求的最后长度length; 需要服务器的IP和端口;
区别:SocketAddress:包含IP和端口;InetSocketAddress设置 IP 再单独设置端口;
客户端总代码:
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//客户端启动需要知道服务器的位置,根据服务器的IP和端口
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//客户端不用指定端口,会自动分配空闲端口;
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
//多次交互
while (true) {
System.out.print(" -> ");
//1.等待控制台输入
String request = scanner.next();
//2.把请求发送到服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.客户端接收响应
DatagramPacket responsePacket = new DatagramPacket(new byte[5000],5000);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.打印日志
System.out.printf("request: %s, response: %s\n", request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8080);
udpEchoClient.start();
}
}
客户端与服务器的网络通信过程:
① 服务器先启动,执行到receive方法后就进行阻塞,等待客户端传输数据 / 请求;
② 客户端运行,等待控制台输入数据并读取,包装成数据报后发送send() 给服务器;
③ 客户端发送数据报后,继续向下执行,到receive方法会阻塞等待服务器返回响应;
在客户端发送请求后的后的同时,服务器会读取客户端的请求,根据请求生成响应,把响应包装成数据报再次send发送给客户端,并打印日志;
④客户端收到服务器的响应,就会解除receive的阻塞,把响应转换为字符串进行打印;
此时服务器会进入下一轮循环,再次等待新的客户端请求;
⑤ 客户端打印完,进入下一轮,再次等待控制台输入新的数据请求;
【注】本机IP地址:127.0.0.1; - 自己与自己发信息;
ServerSocket: 服务器使用的
ServerSocket构造时需要让服务器绑定一个指定的端口,方便客户端能够找到;
Socket: 服务器 和 客户端都可以使用
3.3.1 模拟实现回显服务器
了解InputStream, OutputStream操作字节流对象;
InputStream: 读数据,相当于从网卡读数据;
OutputSteam: 写数据,相当于从网卡发送数据;
首先类TcpEchoServer, 类中创建ServerSocket变量,方便以后操作网卡读取数据;
构造一个参数的构造方法,构造serverSocket对象并指定端口号;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
//与之前的UDP的DatagramSocket中的socket一样,需要绑定端口
serverSocket = new ServerSocket(port);
}
}
然后服务器中的主逻辑:
① 先调用serverSocket的accept方法,等待与客户端建立连接
② 建立连接后,执行客户端连接的方法processConnectin();
核心逻辑代码:
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
t.start();
}
为什么使用线程呢?
服务器启动后,如果不使用线程,当一个客户端占用processConnection(clientSocket)方法,有其他客户端想要再次使用processConnection(clientSocket)方法,就需要一直等待;使用多线程,来一个客户端就创建一个新线程来执行processConnection(clientSocket)方法,就可以多线程使用了;
优化为线程池:
线程的频繁创建和销毁也会带来资源销毁,这里可以优化为线程池;将线程放入线程池,减少线程的创建和销毁;
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
Socket clientSocket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
processConnection(clientSocket)方法具体执行内容:
代码逻辑:
① 读取数据需要对网卡的操作,就用到了InputStream,OutputStream; - try() 中允许写多个流对象;
② 上述字节流,不能清楚读取到哪是一个请求;这里约定服务器是字符串请求,请求之间用\n分隔,所以包装为字符流Scanner , PrintWrite 方便读写数据; - 根据具体情况可更改;
③ 执行客户端请求,与UDP相似,但这里需要判断客户端是否断开连接使用hasNext()方法;
【注】 根据请求计算完响应,发送响应时也应该带上 \n 让客户端分清楚请求 - 使用println加 \n ;(客户端也一样)
clientSocket 是文件,不用了需要关闭;而ServerSocket不需要,它的生命周期长;(循环创建clientSocket,有一个客户端就创建一个,为防止文件资源泄露,需要close()关闭一下文件;)
代码如下:
private void processConnection(Socket clientSocket) {
System.out.printf("客户端上线![%s : %d]\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStrem = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//为了后续操作,字节流包装为字符流 - 可根据具体情况修改;
Scanner scanner = new Scanner(inputStrem);
PrintWriter printWriter = new PrintWriter(outputStream);
//执行具体请求
while (true) {
//1.读取客户端请求
//判断客户端是否关闭了
if(!scanner.hasNext()) {
System.out.printf("客户端下线![%s : %d]\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
//scanner.next读取换行结束,读取一段请求;
String request = scanner.next();
// 读到空白符结束;空白符:\n,空格,制表符;
//2.根据请求做出响应
String response = process(request);
//3.返回响应,响应中加上\n,上述读取一段请求时next不读取\n,这里发送时需要加上;
printWriter.println(response);
printWriter.flush();//刷新,将缓冲区数据立即发送
//4.打印日志
System.out.printf("[%s : %d] request: %s, response: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
为什么InputStream,OutputStream;写到try括号里?
放在try()中,InputStream,OutputStream最后会自动关闭,不用手动close;
为什么需要printWriter.flush() ?
发送的数据,先会写入内存的发送缓冲区,等到缓冲区满了,才会写入网卡;这里使用flush(), 使数据立即写入网卡,不用等待缓冲区满;- 客户端与服务器相同,都需要手动flush()一下;
首先创建TcpEchoClient类,类中创建Socekt操作网卡,创建两个参数的构造方法,让客户端能够找到服务器,与服务器来连接;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String rerverIP, int port) throws IOException {
socket = new Socket(rerverIP,port);
}
}
核心逻辑代码:
与UDP类似;
public void start() {
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//包装为字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scFromSocket = new Scanner(inputStream);//读取网卡请求
while (true) {
//1.等待控制台输入
System.out.println(" --> ");
String request = scanner.next();//这里next以分隔符\n来分隔每段请求
//2.发送请求给服务器
printWriter.println(request);
printWriter.flush();
//3.接收服务器响应
String response = scFromSocket.next();//读取字符流读取字符
//4.打印日志
System.out.printf("[%s : %d] request: %s, response: %s\n",socket.getInetAddress().toString(),
socket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}
TCP客户端-服务器执行过程:
① 服务器先启动,阻塞在accept(),等待客户端连接
② 客户端启动,调用构造方法与服务器建立连接,此时服务器中accept()返回Socket对象;
③ 服务器执行processConnection方法,阻塞到读取客户端请求;
而客户端在调用构造方法后一直往下执行,阻塞等待用户输入,用户输入后读取数据;
④ 客户端读取用户数据发送给服务器,执行到要读取客户端响应后阻塞等待服务器返回响应;
⑤ 服务器接收请求后,读取请求,计算响应,并返回响应;
⑥ 服务器发送响应后重写循环等待下一轮客户端连接;
客户端收到响应,打印结果,再次循环,等待控制台输入;
TCP 客户端总代码:
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
Socket clientSocket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("客户端上线![%s : %d]\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStrem = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//为了后续操作
Scanner scanner = new Scanner(inputStrem);
PrintWriter printWriter = new PrintWriter(outputStream);
//执行具体请求
while (true) {
//1.读取客户端请求
//判断客户端是否关闭了
if(!scanner.hasNext()) {
System.out.printf("客户端下线![%s : %d]\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求做出响应
String response = process(request);
//3.返回响应
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s : %d] request: %s, response: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(8080);
tcpEchoServer.start();
}
}
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!