目录
[1 网络编程](#1 网络编程)
[2 套接字](#2 套接字)
[3 UDP和TCP的区别](#3 UDP和TCP的区别)
[4 UDP数据报套接字编程](#4 UDP数据报套接字编程)
[4.1 API介绍](#4.1 API介绍)
[4.2 通信模型](#4.2 通信模型)
[4.3 示例](#4.3 示例)
[5 TCP流套接字编程](#5 TCP流套接字编程)
[5.1 API介绍](#5.1 API介绍)
[5.2 通信模型](#5.2 通信模型)
[5.3 示例](#5.3 示例)
1 网络编程
网络编程就是指不同的进程之间,通过编程的方式实现网络通信。其最主要的工作是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把报进行解析,从而提取出对应的信息,达到通信的目的。
关于网络编程中的一些基本概念在"网络中的基本概念"这篇文章中有具体的介绍。
2 套接字
套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个套接字就是网络通信的一端,提供了应用层进程利用网络协议交换数据的机制。
其实套接字就是由系统提供的用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
套接字主要分为三种:
- 流套接字:TCP,即Transmission Control Protocol(传输协议控制),传输层协议
- 数据报套接字:UDP,即User Datagram protocol(用户数据报协议),传输层协议
- 原始套接字:用于自定义传输层协议
下面主要进行介绍TCP和UDP。
3 UDP和TCP的区别
UDP
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
- 大小受限:一次最多传输64K
TCP
- 有连接
- 可靠传输
- 面向字节流
- 全双工
- 大小不限
有连接 vs 无连接
这里的连接是一种抽象的,逻辑上的连接,不是说通过一根网线或者是什么实体的东西将发送端和接收端连接起来。
对于UDP来说,UDP协议本身是不会保存对端的信息的,所以如果要直到对端的信息需要自己手动进行保存;
而对于TCP来说,TCP协议本身对对端的信息进行了保存,就不需要自己再进行手动保存了。
可靠传输 vs 不可靠传输
在数据传输的过程中,可能会发生的一种情况就是"丢包",这是因为在进行数据的发送的时候,由于物理层使用光/电信号等进行传输信息,所以可能会受到某些因素的影响。
对于UDP来说,它只管发送数据,其它的就不再进行考虑;
而对于TCP来说,虽然也不能保证数据100%传输成功,但是会尽量提高数据传输成功的概率,并且会提供一些解决"丢包"问题的方法。
面向字节流 vs 面向数据报
对于数据报来说,读写数据的时候是以一个数据报为单位的。传输数据是一块一块的,发送的数据是一个整体,不能分开多次发送,相应地,接收数据也必须整体接收,不能分开接收;
对于字节流来说,读写数据是以字节为单位的。传输数据是基于IO流的,流式数据的特征就是在IO流还没有关闭的情况下,是无边界的数据,可以进行多次发送,也可以分开多次接收。
全双工 vs 半双工
虽然UDP和TCP都是全双工的,但是这里稍微进行一下介绍:
全双工,就是一个通信链路既能读又能写;
半双工,就是一个通信链路要么只能读要么只能写。
4 UDP数据报套接字编程
4.1 API介绍
DatagramSocket
DatagramSocket是UDP Socket,用于发送和接收UDP数据报。
- DatagramSocket构造方法:
|--------------------------|----------------------------------------------------------------------------------|
| 方法签名 | 说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端(客户端的端口号一般都是会变化的,所以不需要指定)) |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务器(因为服务器的端口号指定的话就会比较方便,返回数据的时候可以直接指定这个端口号)) |
- DatagramSocket方法:
|---------------------------------------------|---------------------------------|
| 方法签名 | 说明 |
| void receive(DatagramPacket datagramPacket) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket datagramPacket) | 从此套接字发送数据报包(不会进行阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。
- DatagramPacket构造方法:
|--------------------------------------------------------------------------|-----------------------------------------------------------------------|
| 方法签名 | 说明 |
| DatagramPacket(byte[] buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组中,接收指定长度。 |
| DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来发送数据报,发送的数据在字节数组中,从0到指定长度,address指定目的主机的IP地址和端口号。 |
- DatagramPacket方法:
|--------------------------|------------------------------------------------|
| 方法签名 | 说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机的IP地址;或者从发送的数据报中,获取接收端主机的IP地址。 |
| int getPort() | 从接收的数据报中,获取发送端主机的端口号;或者从发送的数据报中,获取接收端主机的端口号。 |
| byte[] getData() | 获取数据报中的数据。 |
InetSocketAddress
在发送数据报的时候需要传入SocketAddress,该对象使用InetSocketAddress(是SocketAddress的子类)来创建。
- InetSocketAddress构造方法:
|-------------------------------------------------|-------------------------|
| 方法签名 | 说明 |
| InetSocketAddress(InetAddress address,int port) | 创建一个Socket地址,包含IP地址和端口号 |
4.2 通信模型
下面通过一个图来进行简单的介绍UDP是如何实现通信的:

4.3 示例
具体地,我们写一个简单的回显服务器来进行演示:
服务器:
java
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 {
// 使用指定端口号的方法来new一个对象,这个端口号就是这个服务器的端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 死循环 能让服务器一直处理请求
while(true){
// 1.读取请求
// 创建一个DatagramPacket对象,这里的字节数组,此时还是一个空的数组
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
// 输出型参数 receive 这个方法调用之后,接收到的数据报就会存在requestPacket这个对象中
socket.receive(requestPacket);
// 把读到的数据存储为一个字符串,之构造有效的部分
// 这里的长度使用的时 request.getLength() 表示有效数据的长度
// 如果时 request.getData().length 则是这个字节数组的大小,这是不会变的,就是我们设定的 1024
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求进行处理
String response = process(request);
//3.处理之后,把响应通过一个DatagramPacket发送给客户端
// 这里需要给responsePacket客户端的IP地址和端口号,通过requestPacket获取
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d] request: %s , response: %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 server = new UdpEchoServer(9090);
server.start();
}
}
客户端:
java
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
// 先不构造对象,在该类的构造方法中再进行构造对象
private DatagramSocket socket = null;
// 由于UDP是无连接的,所以我们手动保存对端的IP地址和端口号
private String serverIP;
private int serverPort;
// 在创建客户端的时候 把服务器的IP地址和端口号传进去就好了
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
this.serverIP = serverIP;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
//1.从控制台读取内容
// 客户端要发送的请求是从控制台输入的字符串
Scanner scanner = new Scanner(System.in);
while(true){
if(!scanner.hasNext()){
break;
}
String request = scanner.next();
// 2.把内容通过 DatagramPacket 发送,这里在创建DatagramPacket对象的时候需要把IP地址和端口号传进去,不然不知道服务器的IP地址和端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
// 3.通过 一个DatagramPacket对象接收
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
// 依旧是输出型参数,接收到的信息保存到responsePacket中
socket.receive(responsePacket);
// 4.将接收到的信息转化为字符串并进行打印
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
// "127.0.0.1" 就是本机的IP地址 9090 就是之前在服务器代码中指定的服务器的端口号
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
5 TCP流套接字编程
5.1 API介绍
ServerSocket
ServerSocket是创建TCP服务端Socket的API。
- ServerSocket构造方法:
|------------------------|-----------------------------|
| 方法签名 | 说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口号 |
- ServerSocket方法:
|-----------------|---------------------------------------------------------------------|
| 方法签名 | 说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的),有客户端连接后,返回一个服务端Socket对象,并基于该Socket创建与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
Socket
Socket是客户端Socket,或服务端中接收客户端建立连接(即accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接之后,保存对方信息以及用来与对方收发数据的(有点像UDP中的DatagramPacket)
- Socket构造方法:
|------------------------------|-------------------------------------------|
| 方法签名 | 说明 |
| Socket(String host,int port) | 创建一个客户端流套接字Socket,并且与对应IP的主机上,对应端口的进程建立连接 |
- Socket方法:
|--------------------------------|---------------|
| 方法签名 | 说明 |
| InetAddress getInetAddress() | 返回套接字所连接的IP地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
5.2 通信模型

5.3 示例
服务器:
java
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 TcpEchoServer {
private ServerSocket serverSocket = null;
// 绑定服务端端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
// ExecutorService service = Executors.newCachedThreadPool();
System.out.println("服务器启动!");
while(true){
// tcp要先处理客户端发来的连接
// 通过读写clientSocket,和客户端进行通信
// 如果一直没有客户端发来连接,那么accept就会阻塞
Socket clientSocket = serverSocket.accept();
// 由于TCP不能同时运行多个客户端,所以使用多线程/线程池来解决这个问题
// 多线程版
// Thread thread = new Thread(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
// thread.start();
// 线程池版
// service.submit(()->{
// processConnection(clientSocket);
// });
processConnection(clientSocket);
}
}
public void processConnection(Socket socket) throws IOException {
System.out.printf("[%s,%d] 客户端上线!\n",socket.getInetAddress(),socket.getPort());
// 通过Socket中的getInputStream方法和getOutputStream方法进行读写数据
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
// 针对InputStream套了一层壳
Scanner scanner = new Scanner(inputStream);
// 针对OutputStream 套了一层壳
PrintWriter writer = new PrintWriter(outputStream);
// 一直读写数据,直到读写完成
while(true){
// 1 读取请求并且解析
if(!scanner.hasNext()){
System.out.printf("[%s,%d] 客户端下线!",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
// 2 根据请求进行响应
String response = process(request);
// 3 返回响应到客户端
writer.println(response);
// 刷新缓冲区,将数据发送
writer.flush();
// 打印日志
System.out.printf("[%s,%d] request:%s ,response:%s \n",socket.getInetAddress(),socket.getPort(),request,response);
}
}finally {
// 关闭资源
socket.close();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端:
java
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 Socket(serverIP,serverPort);
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
// 和服务器一样都进行套壳
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
// 从控制台读取请求,发送给服务器
while (true){
// 1 从控制台读取请求
String request = scanner.next();
// 2 发送给服务器
writer.println(request);
// 刷新缓冲区,将数据发送
writer.flush();
// 3 读取服务器响应
String response = scannerNet.next();
// 4 打印到控制台
System.out.println(response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}