一、基本概念:
1、首先我们明确两个概念:发送端和接收端
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
※:发送端和接收端只是相对的,只是一次网络数据传输产生数据流后的概念。
2、请求和响应:
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送。
第二次:响应数据的发送。
3、客户端和服务端
服务端:在常见的网络传输场景下,把提供服务的一方进程,称为服务端,可以对外提供服务。
客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是以下两种的提供方式:
①、客户端获取服务资源,即客户端发送请求到服务端,服务端根据客户端的请求进行响应计算,将计算结果返回到客户端。
②、客户端保存资源在服务端,通俗点讲,就是说客户端将数据发往服务端进行存储,服务端随之将存储结果返回(true/false)给客户端。
※:类比银行业务
·银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)
·银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)
二、Socket套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
分类:
主要使用的套接字为如下两种,其余的暂不讨论:
1、使用传输层TCP协议(流套接字):(类似于打电话,接通了才能进行传送信息)
特点:有连接、可靠传输、面向字节流、有接收缓冲区,也有发送缓冲区、大小不限
2、使用传输层UDP协议(数据报套接字):(类似于发微信,直接发送,不考虑对方是否收到)
特点:无连接、不可靠传输、面向数据报、有接收缓冲区,无发送缓冲区,大小受限:一次最多传输64k。
※:当数据的大小超过64k,推荐的两种做法是:一种是将数据进行划分,划分为多个小于64k的数据单元进行传输;另外一种是将UDP协议转化为TCP协议。
Socket编程注意事项:
1、客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同的主机。
2、注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程。
3、Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议也需要考虑,后续我们说明如何设计应用层协议。
4、关于端口被占用的问题
如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。编程过程中我们会详细对这个问题进行处理,大致解决思路如下两种:
①.如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
②.如果需要运行A进程,则可以修改进程B绑定的端口,换位其他没有使用的端口。
我们主要使用第二种解决方案来进行处理,一台电脑会有65536个端口,端口号从0开始到65535指定。当计算机运行时,我们并不清楚哪些端口被哪个进程所占用,此时我们需要通过操作系统来为我们分配空闲的端口供我们使用,以此方式避免端口被占用的问题。
三、UDP数据报套接字编程(DatagramSocket API)
DatagramSocket是UDP Socket,用于发送和接收UDP数据报
DatagramSocket构造方法:
DatagramSocket方法:
DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket构造方法:
DatagramPacket方法:
构造UDP发送的数据报时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建
InetSocketAddress API
案例一(回显服务):
客户端向服务端发送什么,服务端就回应什么。
服务端代码:
public class UdpEchoServer {
//进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提.
private DatagramSocket socket = null;
//此处在构造服务器这边的socket对象的时候,就需要显式的绑定一个端口号,运行程序时指定即可
public UdpEchoServer(int port) throws SocketException {//构造socket对象有很多失败的可能
socket = new DatagramSocket(port);
//端口号是用来区分一个应用程序的~~主机收到网卡上的数据的时候,这个数据该给哪个程序?
}
//启动服务器
public void start() throws IOException {
System.out.println("启动服务器!");
//UDP不需要建立连接,直接接收从客户端来的数据即可~
while (true){
//1.读取客户端发来的请求
// 1.1为了接收数据,需要先准备好一个空的DatagramPacket对象,由receive来进行填充数据
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//把字节数组包装了一下
socket.receive(requestPacket);//receive对这个参数进行填充,“输出型参数”
//把DatagramPacket解析成一个String
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
//2.根据请求计算响应(回显服务,2省略)
String response = process(request);
//3.把响应写回客户端
// 3.1:send方法的参数,也是DatagramPacket,需要把响应数据先构造成一个DatagramPacket再进行发送
// 3.2:这里就不是构造一个空的DatagramPacket了,而是有数据的
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),response.getBytes().length,requestPacket.getSocketAddress());
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
// 3.3:在DatagramPacket构造方法中,指定了第三个参数,表示要把数据发送给哪个地址+端口
socket.send(responsePacket);
}
}
//由于是回显服务,响应和请求就是一样的了
//实际上对于一个真实的服务器来说,这个过程是最复杂的,为了实现这个过程,可能需要n万行代码.....
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
客户端代码:
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//站在客户端的角度:
//源IP:本机IP
//源端口:系统随机分配的端口
//目的IP:服务器的IP
//目的端口:服务器的端口
//协议类型:UDP
public UdpEchoClient(String ip,int port) throws SocketException {
//此处的port是服务器的端口
//客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的~~
socket = new DatagramSocket();
serverIP = ip;
serverPort = port;
}
public void start() throws IOException {
while (true){
//1.先从控制台读取用户输入的字符串
Scanner scanner = new Scanner(System.in);
System.out.print("-> ");
String request = scanner.next();
//2.把这个用户输入的内容,构造成一个UDP请求,并发送
// 构造的请求包含两部分内容
// 1) 数据的内容,request字符串
// 2) 数据要发给谁~服务器的IP+端口
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes(StandardCharsets.UTF_8).length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.从服务器读取响应数据,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.把响应结果显示到控制台上
System.out.printf("req: %s,resp: %s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
//由于服务器和客户端在同一个机器上,使用的IP仍然是127.0.0.1,如果是在不同的机器上,就需要更改这里的IP
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
案例二(字典服务-翻译服务):
客户端向服务器发送英文,服务端将接收到的英文对应的中文回应给客户端。
字典服务的底层逻辑和回显服务基本类似,重要的区别在于对请求的响应处理有所不同,实际开发过程中,对请求的响应处理也是最为复杂的~因此,此处省略客户端代码,客户端代码利用上述回显服务的客户端代码也可~
public class UdpDictServer extends UdpEchoServer{
private HashMap<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//简单构造几个词
dict.put("cat","猫咪");
dict.put("dog","狗狗");
dict.put("bird","小鸟");
dict.put("frog","青蛙");
dict.put("sun","太阳");
dict.put("moon","月亮");
dict.put("fuck","我草");
}
@Override
public String process(String request) {
String ret = null;
if (dict.get(request) == null){
ret = "该词无法查询~";
}else {
ret = dict.get(request);
}
return ret;
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
关于UDP数据报套接字编程的总结:
1.服务器在构造时需要绑定端口号,而客户端则不需要
2.客户端在构造请求的时候,请求包含两部分内容,一个是用户输入的内容,另外一个是服务器的IP和端口号。
3.服务器和客户端在读取收到的数据时,都需要事先准备好一个空的DatagramPacket对象,通过receive来对这个对象进行填充,“输出型参数”
4.服务器在返回响应数据时,构造的就不是一个空的DatagramPacket对象了,而是有数据的,内容包含了数据内容、数据长度,以及requestPacket.getSocketAddress().
requestPacket.getSocketAddress().---->requestPacket是客户端发过来的数据, getSocketAddress:
通过客户端发过来的数据获取客户端的IP+端口号,之后进行返回响应~
四、TCP流套接字编程(ServerSocket API)
ServerSocket API
serverSocket是创建TCP服务端Socket的API
ServerSocket 构造方法:
ServerSocket方法:
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket构造方法:
Socket方法:
扩展知识:TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接,也就是说,长连接可以多次收发数据。
长短连接,区别如下:
1、建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
2、主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
3、两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
案例一(回显服务):
服务器代码(采用多线程,支持多个客户端同时访问服务器):
public class TcpThreadEchoServer {
//listen => 监听
//在Java socket中体现不出来“监听”的含义
//之所以叫listen,是因为操作系统原生的API里有一个操作叫做listen
//private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int post) throws IOException {
serverSocket = new ServerSocket(post);
}
public void start() throws IOException {
System.out.println("服务器启动~");
while (true){
//由于TCP是有连接的,所以需要先建立连接(接电话)
//accept就是在“接电话”,接电话的前提是,有人打~~如果当前没有客户端尝试建立连接,此处的accept就会阻塞
//accept 返回了一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就干了一件事,接电话~
Socket clientSocket = serverSocket.accept();
//改进方法:每次accept成功,都创建一个新的线程,由新线程负责执行这个processConnection方法~
Thread t = new Thread(() ->{
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和相应:
//这里的针对TCP socket 的读写 和文件的读写是一模一样的~~
try (InputStream inputStream = clientSocket.getInputStream()){
try (OutputStream outputStream = clientSocket.getOutputStream()){
//循环的处理每个请求,分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true){
//1.读取请求
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端断开连接~\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处用scanner更方便,如果不用scanner就用原生的InputStream的read也是可以的
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把这个响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
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();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
客户端代码:
public class TcpEchoClient {
//用普通的socket即可,不用ServerSocket
// 此处也不用手动给客户端指定端口号,让系统自由分配
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//这里其实是可以给参数的,但是这里给了之后,含义是不同的~~
//这里传入的ip和端口号的含义表示的不是自己绑定,而是表示和这个ip、端口建立连接!!
//调用这个构造方法,就会和服务器建立连接(打电话拨号了)
socket = new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("和服务器连接成功~");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream()){
try (OutputStream outputStream = socket.getOutputStream()){
while (true){
//要做的事情,仍然是四个步骤
//1.从控制台读取字符串
System.out.println("-> ");
String request = scanner.next();
//2.根据读取的字符串,构造请求,把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();//如果不刷新,可能服务器无法及时看到数据
//3.从服务器读取响应,并解析
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
//4.把结果显示到控制台上
System.out.printf("req: %s,resp: %s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
案例二(字典服务--翻译):
public class TcpDictServer extends TcpThreadEchoServer{
private HashMap<String,String> dict = new HashMap<>();
public TcpDictServer(int post) throws IOException {
super(post);
//简单构造几个词
dict.put("cat","猫咪");
dict.put("dog","狗狗");
dict.put("bird","小鸟");
dict.put("frog","青蛙");
dict.put("sun","太阳");
dict.put("moon","月亮");
dict.put("fuck","我草");
}
@Override
public String process(String request) {
String ret = null;
if (dict.get(request) == null){
ret = "该词无法查询~";
}else {
ret = dict.get(request);
}
return ret;
}
public static void main(String[] args) throws IOException {
TcpDictServer tcpDictServer = new TcpDictServer(9090);
tcpDictServer.start();
}
}
关于TCP流套接字编程的注意事项:
1.由于TCP的有连接的,所以需要先建立连接,当accept到了一个客户端建立连接的请求,就会创建一个新的线程,并且将accept到的客户端构建成一个Socket对象,称为ClientSocket,后续服务器和客户端之间的交互,都是通过clientSocket来完成,ServerSocket只需要将客户端和服务器接通,剩余的工作就交由Socket对象来进行。
2.