网络编程
文章目录
Socket(套接字)
-
Socket API :
是操作系统提供给应用程序的网络编程API
-
介绍一下UDP和TCP:
UDP(User Datagram Protocol)和TCP(Transmission Control Protocol)是两种常用的传输层协议,用于在计算机网络中实现数据传输。
以下是UDP和TCP的对比:
-
连接性:
- UDP是无连接的协议,即在发送数据之前不需要先建立连接。每次发送数据都是独立的,不保留任何传输状态。
- TCP是面向连接的协议,数据传输前必须先建立连接,然后进行可靠的数据传输。
-
可靠性:
- UDP不提供可靠性保证,数据发送后不会确认是否成功到达目标,也不会进行重传。适用于一些实时性要求较高、对数据丢失不敏感的应用场景,如音频和视频传输。
- TCP提供可靠的数据传输,通过序列号和确认机制保证数据的准确性,如果数据丢失或损坏,TCP会自动进行重传,直到数据正确到达目标。
-
数据传输方式:
- UDP采用无连接、不可靠、面向报文的传输方式。每个UDP数据包(报文)独立发送,不会合并和拆分。
- TCP采用面向连接、可靠、面向字节流的传输方式。数据在发送和接收时会进行缓冲,可能会合并多个发送的数据包或拆分成多个接收的数据包。
-
传输效率:
- UDP具有较低的开销和传输延迟,不需要建立连接和维护状态信息。
- TCP的连接建立和维护过程较复杂,会产生较高的开销,但在数据传输时能够保证可靠性。
-
适用场景:
- UDP适用于实时性要求高、对数据丢失不敏感的应用,如音频和视频传输、实时游戏等。
- TCP适用于要求数据传输可靠性较高的应用,如文件传输、网页浏览等。
总的来说,UDP和TCP各有优劣,应根据具体的应用需求选择合适的协议。如果需要快速实时的数据传输,并且对数据丢失不敏感,可以选择UDP。如果需要确保数据的可靠性和完整性,可以选择TCP。
UDP
- UDP的传输方式
UDP(User Datagram Protocol)在传输过程中以数据报(Datagram)为单位进行传输。数据报是UDP传输的基本单位,它包含了数据的有效载荷以及与UDP协议相关的头部信息。
与TCP不同,UDP是一种无连接的传输协议,它不需要在发送和接收数据之前建立连接。每个UDP数据报都是独立的,它们可以以任意顺序发送和接收,并且每个数据报都具有自己的目标地址和端口号。
UDP的数据报没有拆分和重组的过程,它们以原始形式进行传输。UDP只提供了最基本的数据传输功能,它不保证数据的可靠性、有序性和流量控制,也不提供拥塞控制和重传机制。因此,UDP适用于对实时性要求较高、对可靠性要求相对较低的应用场景,如音频和视频传输、实时游戏等。
总结起来,UDP在传输过程中以数据报为单位进行传输,每个数据报包含了数据的有效载荷和与UDP协议相关的头部信息。与TCP不同,UDP的数据报是独立的,没有连接的概念,适用于实时性要求高、可靠性要求相对较低的应用场景。
DatagramSocket
-
UDP编写客户端、服务器
DatagramSocket 使用这个类表示一个Socket对象
在操作系统中,把Socket当作一个文件来处理的
- 普通的文件对应的是硬盘,Socket文件对应的是网卡
- 一个Socket可以和一台主机进行通信,和多个不同的主机进行通信就要有多个Socket对象
DatagramPacket 使用这个类表示UDP传输中的报文
- 构造方法
-
DatagramSocket()
创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
这个版本不是说没有分配端口,而是系统自动分配的端口
-
DatagramSocket(int port)
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
这个版本需要传入一个端口号,此时就是让进程中的Socket对象和端口建立了联系
-
操作Socket的方法
void receive(DatagramPacket p)
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p)
从此套接字发送数据报包(不会阻塞等待,直接发送)
-
void close()
关闭此数据报套接字,释放资源
DatagramPacket
-
★★★DatagramPacket的特点
DatagramPacket(数据报包)是在UDP通信中用于发送和接收数据报的对象。它封装了数据报的内容以及相关的源地址、目标地址、源端口号、目标端口号等信息。 DatagramPacket对象包含两个主要部分:数据(payload)和头部信息(header)。
数据部分是实际要发送或接收的数据,可以是任意类型的字节数组。头部信息包含了源地址、目标地址、源端口号、目标端口号等网络相关信息,以便正确地将数据报发送到目标地址或者从源地址接收。
在UDP通信中,发送方将要发送的数据放入一个DatagramPacket对象中,然后通过DatagramSocket发送该数据报。接收方则创建一个空的DatagramPacket对象,并通过DatagramSocket接收数据报,接收到的数据将填充到该对象中。
-
DatagramPacket
类提供了以下常用的构造方法:-
DatagramPacket(byte[] buf, int length)
:创建一个用于接收数据的DatagramPacket
对象,指定接收数据的缓冲区和缓冲区长度。 -
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
:创建一个用于发送数据的DatagramPacket
对象,指定要发送的数据、目标地址和目标端口号。也可以写成:
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address)
SocketAddress address使用这个类表示IP+端口号
-
服务器和客户端
UDP版本
- 对于端口号
- 客户端的端口号可以由操作系统随机分配一个空闲的端口
- 客户端也可以自己指定端口号,但是指定的端口号可能被其他程序使用了
- 服务器的端口号必须手动绑定,目的是方便客户端找到服务器程序
- 为什么服务器的这里的端口号不怕重复呢,因为服务器是程序猿手里的机器,哪些端口号被使用,程序猿都知道
- 客户端的端口号可以由操作系统随机分配一个空闲的端口
Server
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: Wangduan
* Date: 2023-07-03
* Time: 16:29
*/
public class UdpEchoServer {
//网络通信本质上是要操作网卡
//进行网络通信要先有一个Socket对象
private DatagramSocket socket = null;
//对于服务器,创建Socket对象的同时需要绑定端口号
/**
* 为什么服务器一定要有端口号?
* 答:举例子,我在食堂里面卖熏肉大饼,
* 有一天小王给他的朋友们推荐了食堂的这家熏肉大饼,朋友们问小王在什么位置,
* 小王告诉朋友们在六食堂十八号窗口
* 这个地址就相当于端口号,可以把我比喻成服务器(六食堂十八号窗口就是我的端口号)
* 有了小王提供给朋友们的这个端口号(地址) 才能找到我这个服务器,
*/
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//但是服务器不是给一个人提供服务的,只要有客户端过来,我就要提供服务
while(true){
//1、服务器接收请求并解析请求。
//通过receive方法来读取请求,receive方法的参数是一个输出型参数,要事先构造好一个DatagramPacket对象
//UDP传输数据的基本单位是DatagramPacket,要事先构造好一个DatagramPacket对象
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//因为读取到的是DatagramPacket对象,而我们在代码中更经常使用的是字符串,所以可以用下面的方法来把得到的对象中的数据转换为字符串
//`getData()`方法是Servlet API中的一个方法,用于获取HTTP请求中提交的数据。
// 它返回一个字符串,该字符串包含所有提交的表单数据。如果有多个表单字段具有相同的名称,则返回的字符串将按字母顺序排序。
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2、服务器根据请求的内容,生成响应。
//由于是回显客户端
String response = process(request);
//3、服务器将响应发送回客户端。send的参数也是一个DatagramPacket对象,需要把这个 DatagramPacket对象构造好
//此处还是需要字节数组来进行返回,但是不能是空的字节数组
//指定要发送的数据、目标地址和目标端口号
/**
* 有人来点餐,我给了他一个小票,上面带着一个号码
* 他找了个地方坐下,他坐的地方就是(ip地址,小票上的号码就是端口号)
*/
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//最后这一个参数 是获取到客户端的IP和端口号
socket.send(responsePacket);
//4、打印一下请求处理的结果
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 udpServer = new UdpEchoServer(9090);
udpServer.start();
}
}
Client
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
/**
* 构造客户端 不需要显式绑定端口的
* 可以由操作系统自动分配一个端口
* 客户端的端口号可以由操作系统随机分配一个空闲的端口
*
* - 客户端也可以自己指定端口号,但是指定的端口号可能被其他程序使用了
* @throws SocketException
*/
/**
* 一次通信要有两个ip 两个端口
* 客户端的端口是操作系统随机分配的
* ip是本地ip 127.0.0.1
* 服务器的端口和ip也需要告诉给客户端,方便客户端发送消息给服务器
*
*/
private String serverIp =null;
int serverPort =0;
public UdpEchoClient(String serverIp,int serverPort ) throws SocketException {
socket=new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
Scanner scanner = new Scanner(System.in);
public void start() throws IOException {
System.out.println("客户端启动:");
while(true){
System.out.print(">");
//1、从控制台读取要发送的数据
/*try(InputStream inputStream = new FileInputStream(scanner.next());
OutputStream outputStream = new FileOutputStream(scanner.next())){
String request = scanner.next();
}catch (IOException e){
e.printStackTrace();
}*/
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodBye");
break;
}
//2、把要发送的数据构造成UDP请求,并发送
//构造这个Packet的时候,需要把这个server的Ip和port都传入过来,由于此处的ip地址需要填写的是32位的整数形式
//上述的serverIp是一个字符串,需要使用 InetAddress.getByName(serverIp)来进行一个转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3、读取服务器的UDP响应,并解析
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 {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
TCP
-
TCP的传输方式
TCP在传输过程中的单位可以被理解为以字节传输。TCP是一种面向字节流的传输协议,它将数据分割为一个个字节进行传输。在发送端,数据被分割成较小的数据块,然后打包成TCP段进行传输。在接收端,TCP段被重新组装成原始的字节流数据。
TCP在传输过程中将数据划分为字节流的主要原因是为了提供更高的灵活性和效率。这样可以适应各种数据大小的传输,并且可以灵活地根据网络状况和接收方的接收能力进行调整。
虽然TCP以字节为单位进行传输,但它仍然保留了数据的边界信息。在接收端,TCP根据数据的边界信息将字节重新组装成应用层数据块,以便上层应用程序正确解析和处理数据。
因此,可以说TCP在传输过程中以字节为单位进行数据传输,并通过一系列的协议机制来确保可靠性、有序性和流量控制。
ServerSocket
专门给服务器使用的API
-
构造方法
ServerSocket(int port)
创建一个服务端流套接字Socket,并绑定到指定端口 -
ServerSocket方法
-
Socket accept()
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
-
void close()
关闭此套接字
-
Socket
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
- 构造方法
Socket(String host, int port)
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
- Socket方法
InetAddress getInetAddress()
返回套接字所连接的地址
InputStream getInputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
服务器和客户端
TCP版本
Server
package tcp;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket =null;
Scanner scanner = new Scanner(System.in);
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while(true){
System.out.println("服务器启动!");
//使用这个clientSocket和具体的客户端进行交流
Socket clientSocket = serverSocket.accept();
Thread thread = new Thread(()->{
processConnection(clientSocket);
});
thread.start();
}
}
public void processConnection(Socket clientSocket){
System.out.printf("客户端上线 [%s:%d] ",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述对象和客户端进行通信
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//由于要处理多个请求和响应
while(true){
//1、读取请求
scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
System.out.printf("客户端下线[%s:%d]\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.nextLine();
//2、根据请求构造响应
String response = process(request);
//3、返回响应结果
/*outputStream.write(response.getBytes(),0,response.getBytes().length);
outputStream.write('\n');
outputStream.flush();
*/
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req:%s; resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
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();
}
}
processConnection
方法中的逻辑如下:- 打印客户端上线信息,包括客户端的 IP 地址和端口号。
- 获取客户端连接的输入流和输出流,用于与客户端进行通信。
- 在一个无限循环中,通过
Scanner
对象从输入流中读取客户端发送的请求。 - 如果客户端没有发送请求(
!scanner.hasNext()
),表示客户端已下线,打印客户端下线信息,并跳出循环。 - 从
Scanner
中读取到的请求内容通过process
方法进行处理,得到相应的响应结果。 - 将响应结果通过输出流发送给客户端。
- 打印请求和响应的信息。
- 在循环中的每次迭代,
serverSocket.accept()
方法将阻塞并等待客户端的连接请求。一旦有客户端发起连接请求并被接受,accept()
方法会返回一个新的Socket
对象,该对象表示与客户端的连接。通过这个clientSocket
对象,服务器可以通过其输入流接收客户端发送的数据,并通过输出流向客户端发送响应数据。
Client
package tcp;
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 {
//这个Socket的构造方法可以识别点分十进制
//new 这个对象的同时就会进行客户端和服务器之间的通信了,
//此时的服务器会卡在 accept()的地方
socket = new Socket(serverIp,serverPort);
}
public void start(){
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
System.out.print("> ");
//1、先从键盘读取到要输入的内容
String request = scanner.nextLine();
if(request.equals("exit")){
System.out.println("goodBye!");
break;
}
//2、根据读到的内容构造请求,发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
//printWriter.write(request+"\n");
//
printWriter.println(request);
printWriter.flush();
/*outputStream.write(request.getBytes(),0,request.getBytes().length);
outputStream.write('\n');
outputStream.flush();*/
//3、读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.nextLine();
//4、将返回的响应的内容打印出来
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
-
printWriter.println(request)和 printWriter.write(request)的区别
在使用
PrintWriter
对象写入数据时,printWriter.println(request)
和printWriter.write(request)
有以下区别:-
数据格式:
println()
方法会在写入请求数据后自动添加换行符\n
,而write()
方法不会自动添加换行符。 -
方便性:
println()
方法更方便,特别适合写入字符串数据。它会自动处理字符串的格式化和换行操作。而write()
方法需要手动处理格式化和换行。 -
数据类型:
println()
方法适用于写入字符串数据,它会将传入的参数转换为字符串后写入输出流。而write()
方法可以写入字符数组、字符串、整数等各种数据类型。
在 TCP Echo Server 和 TCP Echo Client 示例中,因为需要发送的请求内容是字符串,所以使用
printWriter.println(request)
更为方便,它可以将字符串写入输出流,并自动添加换行符以便服务器端接收和处理。printWriter.println(request);
这行代码会将
request
字符串写入输出流,并在末尾自动添加换行符。服务器端可以通过Scanner.nextLine()
来读取到完整的请求内容。如果使用
printWriter.write(request)
,则需要手动添加换行符:printWriter.write(request + "\n");
这行代码会将
request
字符串写入输出流,并手动添加换行符。这样服务器端仍然可以通过Scanner.nextLine()
来读取到完整的请求内容。综上所述,对于发送字符串数据,使用
printWriter.println(request)
更加方便和直观。 -