本章要点
- 学会
socket api
原理 - 熟悉
TCP
和UDP
服务器客户端的编写!
概念
Socket
套接字,是由系统提供用于网络通信的技术,是基于TCP/IP
协议的网络通信的基本操作单元。基于Socket
套接字的网络程序开发就是网络编程!
网络编程套接字,是操作系统给应用程序提供的一组API
(socket API
)!
socket
原意插座!
是传输层和应用层通信的桥梁!
我们调用操作系统的Socket
的方法就可通信到传输层!
而我们知道传输层的协议有两种TCP/UDP
!
所以socket
也提供了两组API
用于不同协议点网络编程!
TCP/UDP区别
由于TCP/UDP
两组协议相差较大!因而socket
下对应的API
也差很多!
我们再来介绍一下TCP/UDP
协议的特点!
TCP | UDP |
---|---|
有连接 | 无连接 |
可靠传输 | 不可靠传输 |
面向字节流 | 面向数据报 |
全双工 | 全双工 |
我们分别介绍一下上述的特点是什么意思!
有连接
有连接就是相当于接电话,需要接通后,才能进行消息传递,不想微信/QQ不要接通就可以发消息!
可靠传输
可靠传输就是我们知道我们当前传输的数据对方是否收到了!就类型于钉钉和淘宝的已读功能!如果我们不知道对方是否收到了,那就是不可靠传输!
这里的可靠并不是传统意义的可靠(和数据是否丢失或者盗窃并么有关系)!
面向字节流
面向字节流传输就是,以字节的为单位进行传输,而面向数据报是以数据报为单位进行传输,一个数据报可能为多个字节,只能传输单位个数据报!
全双工
全双工就是一条链路,双向通信,半双工,就是一条链路,单向通信!
UDP数据报套接字编程
我们先来了解一下UDP
协议在socket
对应的API
~
我们进行网络编程需要用到的类都在java.net
包下!
我们UDP socket
编程主要涉及到下面来个类:
DatagramPacket
DatagramSocket
我们来学习下这两个类中的一些重要方法:
DatagramPacket
该类表示数据报包
数据报包用于实现无连接分组传送服务。 仅基于该数据包中包含的信息,每个消息从一台机器路由到另一台机器。 从一台机器发送到另一台机器的多个分组可能会有不同的路由,并且可能以任何顺序到达。 包传送不能保证。
UDP
网络编程等下可能要用到的方法:
构造方法:
通过上述的构造方法我们可以创建不同类型的数据报,有用于接收数据的数据报,还有进行发送数据的数据报!
接收数据的数据报:
DatagramPacket(byte[] buf, int length)
构造一个DatagramPacket
用于接收长度的数据包length
。DatagramPacket(byte[] buf, int offset, int length)
构造一个DatagramPacket
用于接收长度的分组length
,指定偏移到缓冲器中。
上面的两种构造方式都是用于接收数据!相当于一个已经设置好容量的空盘子!
buf
字节数组,确定字节容量!
length
可以存放多大的字节长度
offset
从偏移量位置开始存储
发送数据的数据报
-
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
构造用于发送长度的分组的数据报包length
指定主机上到指定的端口号。 -
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
构造用于发送长度的分组数据报包length
具有偏移ioffset
指定主机上到指定的端口号。 -
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造用于发送长度的分组数据报包 length具有偏移ioffset
指定主机上到指定的端口号。 -
DatagramPacket(byte[] buf, int length, SocketAddress address)
构造用于发送长度的分组的数据报包length
指定主机上到指定的端口号。
可以看到用于发送数据报的构造方法,要传入,IP
和port
端口号等信息才知道这个数据报该发给那个客户端!
InetAddress
IP地址
port
端口号
SocketAddress
:包含了IP
和端口号
DatagramSocket
此类表示用于发送和接收数据报数据包的套接字。
数据报套接字是分组传送服务的发送或接收点。 在数据报套接字上发送或接收的每个数据包都被单独寻址和路由。 从一个机器发送到另一个机器的多个分组可以不同地路由,并且可以以任何顺序到达。
构造方法
DatagramSocket()
构造数据报套接字并将其绑定到本地主机上的任何可用端口。DatagramSocket(DatagramSocketImpl impl)
使用指定的DatagramSocketImpl
创建一个未绑定的数据报套接字。DatagramSocket(int port)
构造数据报套接字并将其绑定到本地主机上的指定端口。DatagramSocket(int port, InetAddress laddr)
创建一个数据报套接字,绑定到指定的本地地址。DatagramSocket(SocketAddress bindaddr)
创建一个数据报套接字,绑定到指定的本地套接字地址。
我们可以传入IP
地址和端口号,让该服务器或者客户端绑定该IP
或者该端口号!
也可以无参,由系统分配端口号!
上面这两个类中还有许多常用方法,我们使用到的时候再介绍!
UDP数据报套接字编程案例
- 回显服务客户端和服务器
我们通过UDP socket
实现一个回显服务的服务器和客户端,就是客户端给服务器发信息,服务器会转发刚刚发送的信息给客户端!
通过这个案例,可以学习UDP
网络编程的流程!
//回显服务器代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//1.源端口 服务器的端口号 需要自己设置
//2.源IP 服务器主机的IP
//3.目的端口 客户端的端口号 服务器接收的数据报中有
//4.目的IP 客户端IP 服务器接收的数据报中!
//5.协议类型 UDP
//编程前提 创建socket实例 用于操作系统中的socket api
DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//设置服务器的端口号
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("启动服务器!");
//UDP不需要建立连接,直接接收请求即可!
while (true){
//为了接收数据 创建了一个指定空间大小的空数据报!
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);//输出型参数 //request 就接收到了数据!
//获取请求,将数据报转化成字符串!
//指定获取数据的起始位置和长度还有编码格式
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
//根据请求计算响应
String response = process(request);
//将响应封装成数据报
//转化成字节数组,字节大小,目的端口和地址getSocktAddress!
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//将客户端的ip 端口 请求 响应 打印
System.out.printf("[%s,%d] req:%s res:%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);
}
}
public String process(String request) {
//回显服务,直接返回请求
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090); //设置服务器端口 9090
udpEchoServer.start(); //启动服务器!
}
}
//回显客户端代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
//1.源IP 客户端本机IP
//2.源端口 客户端端口 可以由系统随机分配
//3.目的IP 服务器IP
//4.目的端口 服务器端口
//5.协议类型 UDP
DatagramSocket socket= null;
String serverIp =null;
int serverPort = 0;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
//服务器不需要指定端口,系统分配!
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
//启动客户端
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while(true){
//写请求 从控制台输入字符!
System.out.print("->");
String request = scanner.nextLine();
//将请求封装成数据报
//这里需要将目的IP和端口传入! 并且需要调用InetAddress.getByName(serverIp)传入目的IP
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
//发送请求给客户端
socket.send(requestPacket);
//接收请求
//给出接收请求的空数据报
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
//将收到的数据报转化成字符串显示
//起始位置,长度,编码方式
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
//输出响应
System.out.println("req:"+request+" res:"+response);
}
}
public static void main(String[] args) throws IOException {
//这里传入的是服务器的IP和端口号
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
//启动服务器
udpEchoClient.start();
}
}
启动服务器:
启动客户端:
我们需要先启动服务器,然后再启动客户端,因为服务器是被动接收的一方,如果客户端没有和服务器发送信息,那么服务器就会一直阻塞在 socket.receive(requestPacket)
等待客户端!
服务器接收到端口号为57266
客户端的请求,发送了响应!
而客户端也接收到了服务器发送的响应,实现了回显服务!
我们知道一个服务器程序一般都有多个客户端访问!
如果想要有其他的客户端访问,该如何做呢!
当我们再次启动客户端程序,会弹出让我们先关闭该程序的窗口!
这样就只能运行一个客户端…
当我们√
上Allow parallel run
就可以同时运行多个实例!
此时我们就有多个客户端了!
服务器也连接了多个客户端!
这就是实现了1对多!
- 字典服务客户端服务器
刚刚的案例,显然没有一点技术,我可改进一下响应,写一个字典服务的服务器,支持中英翻译!
//翻译服务服务器
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
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("pig","小猪");
dict.put("child","小孩");
}
@Override
public String process(String request) {
//返回翻译!
return dict.getOrDefault(request,"暂且不会");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer =new UdpDictServer(9090);
udpDictServer.start();
}
}
我们客户端并不用改变,访问该服务器就可以实现翻译的效果!
TCP流套接字编程
我们知道TCP
是有连接的!
所以我们需要先对客户端建立连接!
我们来学习一下TCP
协议对应的Socket API
!
我们先来学习两个类:
通过这两个类我们就可以对TCP
协议网络编程!
这两个类也在java.net
包下!
ServerSocket
这个类实现了服务器套接字。
服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
服务器套接字的实际工作由SocketImpl
类的实例执行。 应用程序可以更改创建套接字实现的套接字工厂,以配置自己创建适合本地防火墙的套接字!
构造方法:
ServerSocket()
创建未绑定的服务器套接字。ServerSocket(int port)
创建绑定到指定端口的服务器套接字。ServerSocket(int port, int backlog)
创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。ServerSocket(int port, int backlog, InetAddress bindAddr)
创建一个具有指定端口的服务器,侦听backlog
和本地IP
地址绑定。
我们主要用到的是第二个构造方法!我们的服务器一般都需要绑定端口号,便于客户端访问!
这个类一般用于服务器程序!
需要用到的方法
Socket
accept()
倾听要连接到此套接字并接受它!
通过这个方法,我们就可以与客户端建立连接!
返回的Socket
对象,我们后面的操作就对socket
对象进行就好了!
ServerSocket
对象就完成了他的任务和使命了!
就好比之前的电话接线员,连接后就和他没有关系了!剩下的就交个两个打电话的人了!这里的也是剩下的就交给socket
对象就好了!
用通俗的话讲就是,这里的ServerSocket
就做了一件事情,通过网络请求,与客户端建立连接,执行完后就将结果返回给请求者!
Socket
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点。
套接字的实际工作由SocketImpl类的实例执行。 应用程序通过更改创建套接字实现的套接字工厂,可以配置自己创建适合本地防火墙的套接字。
这里的Socket
对象就相当于接电话的两个人,下面的通讯操作,主要通过这个类来实现!
构造方法
Socket()
创建一个未连接的套接字,并使用系统默认类型的SocketImpl。Socket(InetAddress address, int port)
创建流套接字并将其连接到指定IP地址的指定端口号。
我们主要学习这两个构造方法!
第一个无参构造,一般用于服务器!当ServerSocket
对象调用accept
方法后就可以用这个对象接收!因为这个socket
对象是来自客户端,所有已经有了端口号!
而第二个构造方法一般用于创建服务器socket
!
注意:
这里的address
和port
并不是像UDP
一样设置直接的端口号,而是连接到这个IP
和端口号的服务器!
需要用到的方法
void close()
关闭此套接字。InetAddress getInetAddress()
返回套接字所连接的地址。InetAddress getLocalAddress()
获取套接字所绑定的本地地址。
这里一个是连接的服务器地址,一个是自己的地址!
int getLocalPort()
返回此套接字绑定到的本地端口号。InputStream getInputStream()
返回此套接字的输入流。OutputStream getOutputStream()
返回此套接字的输出流。
我们上面的这两个方法就实现了TCP
面向字节流!
通过操作上面的两个对象就可以实现通信,输入和输出了!
TCP
面向字节流网络编程案例
- 回显服务客户端服务器
//回显服务服务器
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;
public class TcpEchoServer {
//1.ServerSocket 只负责建立连接!
ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
//因为TCP是有连接的,我们要先建立连接(接电话)
//ServerSocket只做了一件事情,调用accept方法建立连接
//我们就得到了socketClient客户!!!
//后面就只需要针对socketClient进行操作即可!
//如果没有客户端连接,accept就会阻塞!
Socket socketClient = serverSocket.accept();
processConnection(socketClient);
}
}
public void processConnection(Socket socketClient) {
//TCP面向字节流,所以直接通过文件操作,便可以将数据读写!
try(InputStream inputStream = socketClient.getInputStream()) {
//调用getInputStrem方法获取到请求,用输入字节流接收!
try(OutputStream outputStream = socketClient.getOutputStream()){
//调用getOutStrem方法,用于输出应答!
//通过scanner读取请求更便利!
Scanner scanner = new Scanner(inputStream);
//循环处理每一个请求,返回对应响应!
while(true){
if(!scanner.hasNext()){
System.out.printf("[客户端IP:%s,port:%d] 退出连接\n",socketClient.getInetAddress(),socketClient.getPort());
break;
}
//获取到请求
String request = scanner.next();
//根据请求给出应答
String response = process(request);
//把这个响应给客户端!
//通过printwiter包裹OutputSteam!
PrintWriter printWriter = new PrintWriter(outputStream);
//将请求字符串写入到输出流中!
printWriter.println(response);
printWriter.flush();//刷新缓冲器,让客户端立即获取到响应!
System.out.printf("[客户端IP:%s,port:%d] req:%s,res:%s\n",socketClient.getInetAddress(),socketClient.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭资源!
socketClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
//回显
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.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 {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//客户端
//这里的IP和端口是服务器的,并且这里传入IP和port表示与这个服务器建立连接!
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.print("->");
String request = scanner.next();
//2.构造请求并发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3.读取响应
Scanner scannerResponse = new Scanner(inputStream);
String response = scannerResponse.next();
//把结果打印到控制台!
System.out.printf("req:%s,res:%s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭资源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
启动服务器:
启动客户端:
可以看到,这里我们也实现了回显服务!
当我们多开几个客户端试试!
可以看到客户端并没有收到应答,服务器也没和客户端建立连接!
因为,我们的ServerSocket
对象,调用了accept
后就一直在循环中,如果当前客户端不和服务器断开连接的话,就不会和其他客户端建立连接了!
我们如何才能实现多客户端呢?
我们回顾之前的对线程操作!我们可以有多个线程同时对多个客户端建立连接!!!
那我们进行升级一下变成多进程版本的服务器!
//多线程版本服务器
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;
public class TcpThreadEchoServer{
//1.ServerSocket 只负责建立连接!
ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
//因为TCP是有连接的,我们要先建立连接(接电话)
//ServerSocket只做了一件事情,调用accept方法建立连接
//我们就得到了socketClient客户!!!
//后面就只需要针对socketClient进行操作即可!
//如果没有客户端连接,accept就会阻塞!
Thread thread = new Thread(()->{
Socket socketClient = null;
try {
socketClient = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
processConnection(socketClient);
});
thread.start();
}
}
public void processConnection(Socket socketClient) {
//TCP面向字节流,所以直接通过文件操作,便可以将数据读写!
try(InputStream inputStream = socketClient.getInputStream()) {
//调用getInputStrem方法获取到请求,用输入字节流接收!
try(OutputStream outputStream = socketClient.getOutputStream()){
//调用getOutStrem方法,用于输出应答!
//通过scanner读取请求更便利!
Scanner scanner = new Scanner(inputStream);
//循环处理每一个请求,返回对应响应!
while(true){
if(!scanner.hasNext()){
System.out.printf("[客户端IP:%s,port:%d] 退出连接\n",socketClient.getInetAddress(),socketClient.getPort());
break;
}
//获取到请求
String request = scanner.next();
//根据请求给出应答
String response = process(request);
//把这个响应给客户端!
//通过printwiter包裹OutputSteam!
PrintWriter printWriter = new PrintWriter(outputStream);
//将请求字符串写入到输出流中!
printWriter.println(response);
printWriter.flush();//刷新缓冲器,让客户端立即获取到响应!
System.out.printf("[客户端IP:%s,port:%d] req:%s,res:%s\n",socketClient.getInetAddress(),socketClient.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭资源!
socketClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
//回显
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
我们只更改了一点代码就实现了多个客户端!
我们来学习了线程池,我们在更改成线程池版本!
//线程池版本服务器
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 TcpThreadPoolEchoServer {
//1.ServerSocket 只负责建立连接!
ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();
while(true){
//因为TCP是有连接的,我们要先建立连接(接电话)
//ServerSocket只做了一件事情,调用accept方法建立连接
//我们就得到了socketClient客户!!!
//后面就只需要针对socketClient进行操作即可!
//如果没有客户端连接,accept就会阻塞!
//线程池版本!
Socket socketClient = serverSocket.accept();
pool.submit(new Runnable() {
@Override
public void run() {
}
});
processConnection(socketClient);
}
}
public void processConnection(Socket socketClient) {
//TCP面向字节流,所以直接通过文件操作,便可以将数据读写!
try(InputStream inputStream = socketClient.getInputStream()) {
//调用getInputStrem方法获取到请求,用输入字节流接收!
try(OutputStream outputStream = socketClient.getOutputStream()){
//调用getOutStrem方法,用于输出应答!
//通过scanner读取请求更便利!
Scanner scanner = new Scanner(inputStream);
//循环处理每一个请求,返回对应响应!
while(true){
if(!scanner.hasNext()){
System.out.printf("[客户端IP:%s,port:%d] 退出连接\n",socketClient.getInetAddress(),socketClient.getPort());
break;
}
//获取到请求
String request = scanner.next();
//根据请求给出应答
String response = process(request);
//把这个响应给客户端!
//通过printwiter包裹OutputSteam!
PrintWriter printWriter = new PrintWriter(outputStream);
//将请求字符串写入到输出流中!
printWriter.println(response);
printWriter.flush();//刷新缓冲器,让客户端立即获取到响应!
System.out.printf("[客户端IP:%s,port:%d] req:%s,res:%s\n",socketClient.getInetAddress(),socketClient.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭资源!
socketClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
//回显
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer tcpThreadPoolEchoServer = new TcpThreadPoolEchoServer(9090);
tcpThreadPoolEchoServer.start();
}
}
我们对TCP
和UDP
socket API
网络编程的学习就到此为止,还有其他的内容,我们下次学习!