文章目录
今天,人们广泛使用计算机网络来实现多台计算机之间的联系与数据的交换;而如果想实现位于同一个网络中的计算机之间进行通信,就需要通过编写网络程序来实现,这就是所谓网络编程。即对网络上的主机,通过不同的进程,使用编程的方式来实现网络间的数据传输。
网络通信协议
网络通信协议,实际上就是计算机网络间连接和通信的规则;与交通规则类似,只有通信的双方都遵守这一协议,才能顺利完成数据的交换和信息的交流。
网络通信的协议有许多,但目前应用最为广泛的是TCP/IP协议。这里不对协议的内容做展开阐述,只是基于网络通信协议学习如何进行网络编程;
网络编程的相关概念
- 客户端和服务器
客户端:获取服务的一方,主动发送网络数据;
服务器:提供服务的一方,被动接收网络数据;
- 发送端和接收端
发送端:网络通信中的源主机,数据的发送方;
接收端:网络通信中的目的主机,数据的接收方;
- 请求和响应
请求:客户端给服务器发送的数据;
响应:服务器返回给客户端的数据;
- 客户端与服务器的交互方式
一问一答:客户端发送一个请求,服务器返回一个响应;
多问一答:客户端发送多个请求,服务器返回一个响应;
一问多答:客户端发送一个请求,服务器返回多个响应;
多问多答:客户端发送多个请求,服务器返回多个响应;
Socket套接字
Socket套接字是基于TCP/IP协议的网络通信的基本操作单元,通过对网络连接进行高级的抽象,提供了操作网络连接的便利性,Socket就像是网络编程的标准。使用Socket接口,即使是在不同系统上运行不同的TCP/IP,也同样可以成功运行;
三类不同的套接字
套接字依据传输层协议可以划分为3类:
- 流套接字:使用传输层TCP协议;
- 数据报套接字:使用传输层UDP协议;
- 原始套接字;
Socket编程
UDP版Socket编程
- 服务器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
public class UdpServer {
//创建UDP服务器,首先打开一个socket文件
private DatagramSocket socket=null;
public UdpServer(int port) throws SocketException {
//创建UDP服务器,指定端口,可以发送和接收数据报
socket=new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//使用循环不断接收客户端UDP数据报
while(true){
//使用DatagramPacket的构造方法,创建一个字节数组用于接收数据,
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
//使用receive方法从此套接字接收数据报
socket.receive(requestPacket);
//对客户端发来的请求进行解析,把DatagramPacket转化成String类型
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//对请求进行处理,进行响应
String response=process(request);
//构造响应对象,为DatagramPacket对象,
DatagramPacket responsePacket =new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//将响应从此套接字发送数据报包
socket.send(responsePacket);
System.out.printf("[%s:%d] req=%s; resp=%s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
request, response);
}
}
//单独的一个方法用来处理响应,这里是一个回显服务器,直接返回请求内容即可
public String process (String req){
return req;
}
public static void main(String[] args) throws IOException {
//实例化一个UDP服务器,同时指定端口
UdpServer server=new UdpServer(8000);
server.start();
}
}
DatagramPacket类
由于UDP是一种面向无连接的协议,因此,通信时发送端和接收端不用建立连接;
JDK中提供了一个DatagramPacket类,用于封装UDP通信中发送或接收的数据;
DatagramPacket类的构造方法
- DatagramPacket(byte[]buf, int length):指定封装数据的字节数组和数据大小,只用于接收端;
- DatagramPacket(byte[]buf, int length,InetAddress addr,int port):指定封装数据的字节数组,数据大小,目标IP地址及端口号,常用于发送端;
- DatagramPacket(byte[]buf, int offset, int length):指定字节数组与数据大小的同时增加了一个参数,指明接收到的数据在放入数组时从指定位置开始;
- DatagramPacket(byte[] buf, int offset, int length,InetAddress addr,int port):与上面一种类似,另外指明了目的IP地址和端口号;
DatagramPacket类的常用方法
- InetAddress getAddress():返回发送端或接收端的IP地址;
- int getPort():返回发送端或接收端的端口号;
- byte[] getData():返回将要接收或者即将发送的数据;
- int getLength():返回接收或将要发送数据的长度;
下面是客户端一方的Socket编程:
- 客户端
import jdk.nashorn.internal.ir.WhileNode;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpClient {
private DatagramSocket socket=null;
public UdpClient() throws SocketException {
//创建UDP客户端,不需要手动指定端口号,一般由操作系统自动分配
socket=new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while (true){
System.out.print("--->");
//从控制台读取数据
String request=scanner.next();
//创建一个要发送的数据包,包括发送的数据,数据长度,接收端的IP地址以及端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName("127.0.0.1"), 8000);
//发送数据包给服务器
socket.send(requestPacket);
//创建数组用于读取数据
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
//接收数据报,
socket.receive(responsePacket);
//将响应数据转化为String
String response =new String(responsePacket.getData(),0, responsePacket.getLength());
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpClient client=new UdpClient();
client.start();
}
}
DatagramSocket类
DatagramSocket类用于发送和接收DatagramPacket数据包,就类似于“码头”的作用;
DatagramSocket类的构造方法
- DatagramSocket() :用于创建发送端的DatagramSocket对象;
- DatagramSocket(int port):指定了端口号,可以用于发送端或者接收端;
- DatagramSocket(int port,InetAddress addr):指定了端口号和相关的IP地址;
DatagramSocket类的常用方法
- void receive(DatagramPacket p):将接收到的数据填充到DatagramPacket数据包中,并返回,在未接收到数据包时处于阻塞状态;
- void send(DatagramPacket p):用于发送DatagramPacket数据包;
- void close():关闭当前的套接字,释放资源;
TCP版Socket编程
- 服务器
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.security.spec.RSAOtherPrimeInfo;
import java.util.Scanner;
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();
processConnect(clientSocket);
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
ServerSocket类
ServerSocket类是JDK为开发TCP程序创建服务器端时提供的类,该类的实例对象可以实现一个服务器端的程序;
ServerSocket类的构造方法
- ServerSocket( ):只是创建一个ServerSocket对象,没有绑定端口号,不可以直接使用;
- ServerSocket(int port):创建对象的同时绑定端口号;
- ServerSocket(int port,int backlog):第二个参数表示用于在服务器忙时,可以与之保持连接请求的等待客户数量,默认为50;
- ServerSocket(int port,int backlog,InetAddress bindAddr):在上面的基础上,指定了相关的IP地址;
ServerSocket类的常用方法
- Socket accept():用于等待客户端的连接,在未连接之前处于阻塞状态;
- InetAddress getInetAddress():返回一个InetAddress对象,即ServerSocket绑定的IP地址;
- void close() :关闭套接字,结束本次通信;
下面是客户端:
- 客户端
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 TcpClient {
private Socket socket=null;
public TcpClient() throws IOException {
//创建客户端,通过IP地址和端口号与服务器建立连接
socket=new Socket("127.0.0.1",8000);
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scanner1=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
//循环实现输入
while (true){
System.out.println(">---");
//从控制台读取输入数据
String request=scanner.next();
//发送请求给服务器
printWriter.println(request);
//刷新缓冲区
printWriter.flush();
//从服务器读取响应
String response=scanner1.next();
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
}
public static void main(String[] args) throws IOException {
TcpClient client=new TcpClient();
client.start();
}
}
Socket类
Socket类用于实现TCP客户端程序;
Socket类的构造方法
- Socket():仅仅创建了Socket对象,即只创建了客户端对象,没有连接服务器;
- Socket(String host,int port):创建对象的同时,根据指定的地址和端口号去连接服务器;
Socket类的常用方法
- int getPort():返回Socket对象与服务器端连接的端口号;
- InetAddress getInetAddress() :获取Socket对象绑定的本地IP地址;
- InputStream getInputStream():返回套接字的输入流对象,如果该对象是由服务端的对象返回,就用于读取客户端发送的数据,反之;
- void close() :关闭套接字,结束本次通信;
- OutputStream getOutputStream() :返回输出流对象;
TCP中的长连接与短连接
短连接:即每次接收到数据并返回响应后便关闭连接,一次连接只进行一次数据交互;
长连接:即交互双方不停进行交互,不关闭连接;
短连接与长连接的区别:
由于短连接在每次请求和响应时,都要建立连接再关闭连接,因此长连接的效率相对更高;
短连接一般是客户端主动向服务端发送请求,长连接则两方都可以主动发送请求;
短连接更加适用于客户端请求频率较低的场景,而长连接更加适用于客户端与服务器交互频繁的场景;
TCP版Socket编程的改进
上面所提供的TCP版Socket编程中,关于服务器的实现,仅仅是提供了一种单线程的方式。但是在实际的应用场景中,需要一台服务器同时为多台客户端提供服务的情况时有发生,因此我们尝试实现一种多线程版的服务器端程序:
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.security.spec.RSAOtherPrimeInfo;
import java.util.Scanner;
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();
//多线程版
Thread t=new Thread(()->{
try{
processConnect(clientSocket);
} catch (IOException e){
e.printStackTrace();
}
});
t.start();
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
客户端程序与上面TCP版的Socket编程一致,下面是实际的运行情况:
解决了为多台客户端服务的问题,我们的实现方式又带来了新的问题,在有众多客户端请求的情况下,使用普通的线程创建方式,必定会频繁的创建和销毁线程,因而有可能会加大系统的开销,所以我们可以尝试线程池的方式:
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.security.spec.RSAOtherPrimeInfo;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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("服务器启动!");
//创建线程池
ExecutorService service= Executors.newCachedThreadPool();
while (true){
//返回一个与客户端连接的对象
Socket clientSocket=serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnect(clientSocket);
}catch (IOException e){
e.printStackTrace();
}
}
});
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
至此,有关网络编程的知识就基本结束啦~~
最后是关于IDEA在不结束程序的情况下再打开同一程序的方法:
首先在这里的下拉框选择要同时打开哪个程序:
点击下面选中的编辑栏:
来到这个界面,点击蓝色字体Modify options:
勾选下面选中的一栏:
点击apply即可;