一、套接字
Socket套接字,是由系统提供用于网络通信的技术(操作系统给应用程序提供的一组API叫做Socket API),是基于TCP/IP 协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
socket可以视为是应用层和传输层之间的通信桥梁; 传输层的核心协议有两种:TCP,UDP ;socket API也有对应的两组,由于TCP和UDP协议差别很大,因此,这两组API差别也挺大。
分类: Socket 套接字主要针对传输层协议划分为如下三类:
- 流套接字:使用传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议;
TCP的特点:
- 有连接:像打电话,得先接通,才能交互数据;
- 可靠传输:传输过程中,发送方知道接收方有没有收到数据.(打电话就是可靠传输);
- 面向字节流:以字节为单位进行传输.(非常类似于文件操作中的字节流);
- 全双工:一条链路,双向通信;
- 有接收缓冲区,也有发送缓冲区。
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
- 数据报套接字:使用传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
UDP的特点:
- 无连接:像发微信,不需要接通,直接就能发数据;
- 不可靠传输:传输过程中,发送方不知道接收方有没有收到数据.(发微信就是不可靠传输);
- 面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接收必须是一个完整的数据报,不能是半个,也不能是一个半;
- 全双工:一条链路,双向通信;
- 有接收缓冲区,无发送缓冲区;
- 大小受限:一次最多传输64k;
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
- 原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
二、UDP数据报套接字编程
UDPSocket 中,主要涉及到两类:DatagramSocket、DatagramPacket ;
DatagramSocket API
DatagramSocket 创建了一个UDP 版本的Socket 对象,用于发送和接收UDP数据报,代表着操作系统中的一个socket文件,(操作系统实现的功能–>)代表着网卡硬件设备的抽象体现。
DatagramSocket 构造方法:
方法签名 | 方法说明 |
---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) | DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法签名 | 方法说明 |
---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) | void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) | void close() | 关闭此数据报套接字 |
DatagramPacket API
代表了一个UDP数据报,是UDP Socket发送和接收的数据报,每次发送/接收数据报,都是在传输一个DatagramPacket 对象。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) | DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
方法签名 | 方法说明 |
---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 | int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 | byte[] getData() | 获取数据报中的数据 |
构造UDP 发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 | 方法说明 |
---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
示例1:写一个简单的客户端服务程序,回显服务(EchoSever )
构建Socket对象有很多失败的可能:
- 端口号已经被占用,同一个主机的两个程序不能有相同的端口号(这就好比两个人不能拥有相同的电话号码);
此处,多个进程不能绑定同一个端口号,但是一个进程可以绑定多个端口 ,(这就好比一个人可以拥有多个手机号),一个进程可以创建多个Socket对象,每个Socket都绑定自己的端口。 - 每个进程能够打开的文件个数是有上限的,如果进程之间已经打开了很多文件,就可能导致此时的Socket文件不能顺利打开;
这个长度不一定是1024,假设这里的UDP数据最长是1024,实际的数据可能不够1024.
这里的参数不再是一个空的字节数组了,response是刚才根据请求计算的得到的响应,是非空的,DatagramPacket 里面的数据就是String response的数据。
response.getBytes().length :这里拿到的是字节数组的长度(字节的个数),而response.length得到的是字符的长度。
五元组
一次通信是由5个核心信息描述的:源IP、 源端口、 目的IP、 目的端口、 协议类型。
站在客户端角度:
- 源IP:本机IP;
- 源端口:系统分配的端口;
- 目的IP:服务器的IP;
- 目的端口:服务器的端口;
- 协议类型:TCP;
站在服务器的角度:
- 源IP:服务器程序本机的IP;
- 源端口:服务器绑定的端口(此处手动指定了9090);
- 目的IP:包含在收到的数据报中(客户端的IP);
- 目的端口:包含在收到的数据报中(客户端的端口);
- 协议类型:UDP;
服务器
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
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){
DatagramPacket datagramPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(datagramPacket);
String request = new String(datagramPacket.getData(),0,datagramPacket.getLength(),"UTF-8");
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,datagramPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req :%s,resp: %s\n",responsePacket.getAddress().toString(),
responsePacket.getPort(),request,response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer sever = new UdpEchoServer(9090);
sever.start();
}
}
这里就是系统自动给客户端分配的端口; 客户端可以有很多的,一个服务器可以给很多客户端提供服务;
客户端
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIP;
private int severPort;
public UdpEchoClient(String ip,int port) throws SocketException {
socket = new DatagramSocket();
severIP = ip;
severPort = port;
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("-> ");
String request = sc.next();
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(severIP),severPort);
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.printf("req :%s,resp: %s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
写一个翻译程序,请求是一些简单的英文单词,响应是英文单词对应的翻译:
客户端不变,服务器代码进行调整:主要是调整process方法; 读取请求并解析,把响应写回到客户端,这两步是一样的,关键逻辑就是:根据请求处理响应。
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", "小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "该词无法被翻译!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
三、TCP流套接字编程
TCP API 也涉及到两个核心的类: ServerSocket :专门给TCP服务器用; Socket :即需要给服务器用,又需要给客户端用; 主要通过这样的类,来描述一个socket文件即可,而不需要专门的类表示"传输的包";面向字节流(以字节为单位传输的)。
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
方法签名 | 方法说明 |
---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 | void close() | 关闭此套接字 |
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法签名 | 方法说明 |
---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连 |
Socket 方法:
方法签名 | 方法说明 |
---|
InetAddress getInetAddress() | 返回套接字所连接的地址 | InputStream getInputStream() | 返回该套接字的输入流 | OutputStream getOutputStream() | 返回该套接字的输出流 |
TCP服务器
这里之所以分成了两步,就是因为要建立连接.一个专门负责建立连接,一个专门负责数据通信。
package UDP;
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 {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s %d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
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 printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s %d] req: %s, reps %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 {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
TCP客户端
对于UDP的socket来说,构造方法指定的端口,表示自己绑定哪个端口; 对于TCP的ServerSocket来说构造方法指定的端口,也是表示自己绑定哪个端口; 对于TCP的Socket来说构造方法指定的端口,表示要连接的服务器的端口.
UDP没有用多线程因为: UDP不需要处理连接,UDP只要一个循环,就可以处理所有客户端的请求.但是此处, TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环.里层循环,就会影响到外层循环的进度了. 主线程,循环调用accept .当有客户端连接上来的时候,就直接让主线程创建一个新线程。由新线程负责对客户端的若干个请求,提供服务.(在新线程里,通过while循环来处理请求).这个时候,多个线程是并发执行的关系(宏观上看起来同时执行),就是各自执行各自的了,就不会相互干扰(也要注意:每个客户端连上来都得分配一个线程)
多线程版本,可以同时与多个客户端进行通信:
package UDP;
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 TcpThreadEchoSever {
private ServerSocket serverSocket = null;
public TcpThreadEchoSever(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(()->{
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
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 printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req: %s, reps %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 {
TcpThreadEchoSever tcpThreadEchoSever = new TcpThreadEchoSever(9090);
tcpThreadEchoSever.start();
}
}
TCP服务器线程池版本:
package UDP;
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 TcpThreadPollEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadPollEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
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 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 {
TcpThreadPollEchoServer server = new TcpThreadPollEchoServer(9090);
server.start();
}
}
TCP版本查字典的服务器
package UDP;
import java.io.IOException;
import java.util.HashMap;
public class TcpDictSever extends TcpThreadPollEchoServer {
private HashMap<String,String> map = new HashMap<>();
public TcpDictSever(int port) throws IOException {
super(port);
map.put("cat","猫");
map.put("pig","猪");
map.put("dog","狗");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"当前词组无法找到!");
}
public static void main(String[] args) throws IOException {
TcpThreadPollEchoServer server = new TcpThreadPollEchoServer(9090);
server.start();
}
}
以上。
|