网络编程中的基本概念
发送端和接收端 在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端
客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源在服务端
Socket套接字
Socket套接字是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元
分类
Socket套接字主要针对传输层协议,操作系统提供一组api => socket api(传输层给应用层提供)
传数层的两个核心协议TCP和UDP
TCP和UDP协议
TCP协议特点:有连接,可靠传输,面向字节流,全双工
UDP协议特点:无连接,不可靠传输,面向数据报,全双工
有连接和无连接
这是抽象的概念,虚拟的/逻辑上的连接
要进行网络通信需要物理连接(网线啥的)
对于TCP来说,TCP协议中就保存了对端的信息
A和B通信,A和B建立连接,让A保存B的信息,B保存A的信息(彼此之间谁是和它建立连接到的
那个)
对于UDP来说,UDP协议本身不保存对方的信息,就是无连接
可靠传输与不可靠传输
网络上数据是非常容易出现丢失的情况(丢包),光信号/电信号,都可能受到外界的干扰
可靠传输的意思不是保证数据包100%到达,而是尽可能的提高传输成功的概率,如果出现丢包就
能感知到
不可靠传输只是把数据发了就不管了
面向字节流和面向数据报
面向字节流读写数据的时候是以字节为单位,此时支持任意长度,但可能会出现粘包问题
面向数据报读写数据的时候是以一个数据报为单位(不是字符),一次必须读写一个UDP数据报,不
能是半个 ,不存在粘包问题但可能会出现长度限制
全双工和半双工
一个通信链路支持双向通信(能读,能写)
一个通信链路只支持单向通信(要么读,要么写)
Socket API网络编程
socket api网络编程本身是操作系统的功能,计算机中的文件通常是一个广义的概念,文件还能指
代一些硬件设备(操作系统管理硬件设备,也是抽象成文件进行统一管理)
操作网卡不好直接进行操作,把操作网卡传换成操作socket文件,socket文件相当于"网卡的遥控
器"
socket文件可以进行网卡操作,操作网卡的时候流程和普通文件差不多
打开(会在文件描述符表中分配一个表项)->读写->关闭
UDP数据报套接字编程
API 介绍
DatagramSocket
DatagramSocket 是UDP Socket用于发送和接收UDP数据报
DatagramSocket构造方法
用于打开文件
|--------------------------|----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任 意一个随机端口(一般用于客户端) |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指 定的端口(一般用于服务端) |
创建socket的时候就会关联上一端口号,使用端口号区分主机上不同的应用程序
DatagramSocket方法
|--------------------------------|----------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该 方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发 送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket 构造方法
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的 数据保存在字节数组(第一个参数buf)中,接收指定 长度(第二个参数length) |
| DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的 数据为字节数组(第一个参数buf)中,从0到指定长 度(第二个参数length)。address指定目的主机的IP 和端口号 |
UDP数据包的载荷数据就可以通过构造方法来指定
DatagramPacket 方法
|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发 送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从 发送的数据报中,获取接收端主机端口号 |
| byte[] getData( | 获取数据报中的数据 |
构造UDP发送的数据报时需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress
InetSocketAddress ( SocketAddress 的子类 )构造方法:
|-----------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
UDP代码实例
回响服务器
java
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){
// 循环一次, 就相当于处理一次请求.
//1.读取请求并解析
// DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
// 把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求,计算响应(服务器最关键的逻辑)
//但是此处写的是回响服务器,这个环节相当于省略了
String response=process(request);
//3.把相应返回给客户端
// 根据 response 构造 DatagramPacket, 发送给客户端.
// 此处不能使用 response.length()
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
// 此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
// 需要指定 目的 ip 和 目的端口.
socket.send(responsePacket);
//打印日志
System.out.printf("[%s:%d] req: %s , rep %s\n",requestPacket.getAddress(),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();
}
}
socket.receive(requestPacket)是输出型参数,调用之前先构造空的(不是null),把对象传递到
receive里面,receive就会把数据从网卡读出来填充到参数中
response.getBytes().length是描述String中字节的个数
response.length是描述String中字符的个数

当把hasNextLine()改成hasNext()时,只能进行字符输入,输入字符串的话就会被分开

a) 构造 DatagramPacket 对象. DatagramPacket 就代表 UDP 数据包. 报头 + 载荷(new 字节数组保存)
b) 调用 receive ,输出型参数
c) 把 udp 数据包载荷取出来, 构造成一个 String
- 通过 requestPacket.getData() 拿到 DatagramPacket 中的字节数组~~
- requestPacket.getLength() 拿到有效数据的长度
- 根据字节数组, 构造出一个 String

-
response.getBytes()拿到字符串中的字节数组 -
response.getBytes().length拿到字节数组的长度 而不是使用字符串长度(单位 字符) -
requestPacket.getSocketAddress()拿到客户端的 IP 和端口号,这个方法返回的对象中同时包含 IP 和端口
DatagramPacket 有三个方法.
- getAddress 只拿到 IP
- getPort 只拿到端口
- getSocketAddress 同时拿到 IP 和端口 (IP 和端口通过一个 InetAddress 对象表示)
客户端
java
public class UdpEchoClient {
private DatagramSocket socket=null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
this.serverIp=serverIp;
this.serverPort=serverPort;
socket=new DatagramSocket();
}
public void start() throws IOException {
Scanner scan=new Scanner(System.in);
while (true){
//1.从控制台读取用户输入的内容
System.out.println("输入要发送的内容");
if (!scan.hasNextLine()){
break;
}
//2.把请求发送给服务器,需要构造DatagramPacket对象
// 构造过程中,不光要构造载荷,还要设置服务器的IP和端口号
String request= scan.nextLine();
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
//3.发送数据报
socket.send(requestPacket);
//4.接收服务器响应
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//5.从服务器读取数据进行解析,打印出来
String response=new String(responsePacket.getData(), 0,requestPacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}

new DatagramSocket()中不能填写serverPort,必须使用无参数的版本
客户端访问服务器,serverIP 是目的ip, serverPort 是目的端口源 ip 客户端所在的主机 ip, 源端口, 应
该是随机搞一个端口 (操作系统分配空闲端口)

构造请求的数据报
1.载荷
2.目的IP和目的端口

构造 client 对象指定服务器的 IP 和端口.
127.0.0.1 特殊的 IP 环回 IP 表示当前这个主机,无论你主机的 IP 真实是啥, 都可以使用 127.0.0.1
代替类似于 this,由于此时客户端和服务器在同一个主机上, 就可以使用 127.0.0.1 来访问 如果是
不同主机, 就需要填写其他的 IP 了.
TCP流套接字编程
TCP的一个核心特点就是面向字节流,读写数据基本单位就是字节byte
API 介绍
ServerSocket
ServerSocket 是创建TCP服务端Socket的API,专门给服务器使用
ServerSocket 构造方法:
|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
|-----------------|-------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端 连接后,返回一个服务端Socket对象,并基于该 Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服
务端Socket。服务器和客户端都可以使用
Socket 构造方法
|-------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机 上,对应端口的进程建立连接 |
Socket 方法
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP代码示例
服务器
java
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) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner scan=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while(true){
if (!scan.hasNext()){
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
String request=scan.nextLine();
String response=process(request);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req:%s,reps:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
客户端
java
public class TcpEchoClient {
private Socket socket=null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
socket=new Socket(serverIP,serverPort);
}
public void start(){
Scanner scan=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scanNext=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while (true){
String request=scan.nextLine();
writer.println(request);
writer.flush();
String response=scanNext.nextLine();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}

创建 socket 对象就会在底层和对端建立 tcp 的连接.
(所谓连接, 记录了对端的信息)
服务器的 ip 和端口这样的信息, 不需要自己创建变量保存了.
直接 tcp 内部就记住了
对于服务器和客户端中Socket的区别


这俩 socket 对象, 绝对不是同一个对象 (分别在不同进程中, 甚至在不同主机上)
flush

这个操作只是把数据放到"发送缓冲区"中,还没有真正写入到网卡中去

flush()方法是用来冲涮缓冲区的
UDP和TCP套接字编程区别
UDP 就是以 DatagramPacket 作为单位的
TCP 则是字节为单位实际上一个请求往往是由多个字节构成的,可能出现粘包问题
服务器引入多线程
如果只是单个线程, 无法同时响应多个客户端.此处给每个客户端都分配一个线程.
当启用多个线程时发现线程会出现卡住,如果不关闭第一个线程第二个线程迟迟无法运行


引入多线程之后,就可以实现向多个客户端发送消息
服务器引入线程池
为了避免频繁创建销毁线程, 也可以引入线程池.

这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.