1 基础知识
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析.
认识端口号
端口号(port)是传输层协议的内容.
- 端口号是一个32位的整数;
- 端口号标识一个进程,告诉操作系统,当前这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能标识网络上的某一台主机上的某一个进程;
- 一个端口号只能被一个进程占用;
理解 “端口号” 和 “进程ID”
系统编程中pid表示唯一一个进程;端口号也是唯一表示一个进程;
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的, 要发给谁”;
认识TCP协议
我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
2 Socket 编程
2.1 socket 常见API
DatagramSocket类:
简单的UDP网络程序
实现一个简单的服务端接收客户端输入的信息然后在服务端显示。
UDP服务端
package frank.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
/**
* 接收客户端输入的字符串,在服务端输出
*
* @author haozhang
* @date 2020/03/09
*/
public class UdpServer {
/**
* 定义UDP端口号
*/
public static final int PORT = 30000;
/**
* 每个数据报最大位4k
*/
public static final int DATA_LEN = 4096;
/**
* 接收网络数据的字节数组
*/
private byte[] buffIn = new byte[DATA_LEN];
/**
* 以指定字节数组创建准备接受数据的DatagramPacket
*/
private DatagramPacket packetIn = new DatagramPacket(buffIn, buffIn.length);
/**
* 定义一个用于发送的DatagramPacket对象
*/
private DatagramPacket packetOut;
public void start() {
try {
DatagramSocket socket = new DatagramSocket(PORT);
//客户端输入的英文单词
String word = null;
//获取客户端对象,通过该对象将数据写给对方,否则没有目的地
SocketAddress address = null;
//返回给客户端数据
byte[] receiveData = null;
System.out.println("服务器启动了");
while (true) {
//读取socket中的数据,将数据封装到packetIn中
socket.receive(packetIn);
//将byte数组转换为字符串,需要去掉后面的空格
word = new String(buffIn, 0, buffIn.length).trim();
System.out.println("客户端的输入是:" + word);
address = packetIn.getSocketAddress();
//检查退出条件
if ("down".equals(word)) {
receiveData = "服务器关闭了,请重试".getBytes();
//构建packetOut 发送数据
packetOut = new DatagramPacket(receiveData, receiveData.length, address);
socket.send(packetOut);
}
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器关闭了");
}
}
public static void main(String[] args) {
new UdpServer().start();
}
}
UDP客户端
package frank.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* Demo class
*
* @author haozhang
* @date 2020/03/09
*/
public class UdpClient {
/**
* 定义发送数据报的目的地
*/
public static final int PORT = 30000;
public static final String IP = "127.0.0.1";
/**
* 每个数据报的最大大小
*/
public static final int DATA_LEN = 4096;
/**
* 定义接收数据字节的数组
*/
private byte[] inBuff = new byte[DATA_LEN];
/**
* 创建接收回复数据DatagramPacket对象
*/
private DatagramPacket packetIn = new DatagramPacket(inBuff, inBuff.length);
/**
* 定义一个用于发送的DatagramPacket对象
*/
private DatagramPacket packetOut = null;
public void start(){
try {
DatagramSocket socket = new DatagramSocket();
//初始化发送用的 DatagramSocket
InetAddress ip = InetAddress.getByName(IP);
packetOut = new DatagramPacket(new byte[0], 0, ip, PORT);
//创建键盘输入流
Scanner scanner = new Scanner(System.in);
//从键盘读入
String key = null;
//键盘输入的字符对应的byte数组
byte[] keyBuff = null;
System.out.println("请输入数据:");
while (scanner.hasNextLine()) {
key = scanner.nextLine();
if ("exit".equals(key)) {
System.out.println("客户端退出了");
break;
}
//输入的 字符串 --> 字节数组
keyBuff = key.getBytes();
//设置发送用的 DatagramPacket 里的字节数据
packetOut.setData(keyBuff);
//发送数据报
socket.send(packetOut);
//读取socket中的数据,读到的数据放在inPacket所封装的字节数组里
socket.receive(packetIn);
System.out.println(new String(inBuff, 0, packetIn.getLength()));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new UdpClient().start();
}
}
实现英译汉服务器
通过上面的UDP服务端进行修改就可以实现英汉互译的服务器,服务端代码需要调整,客户端则不变,代码如下:
package frank.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.Map;
/**
* Demo class
*
* @author haozhang
* @date 2020/03/09
*/
public class UdpDictServer {
/**
* 定义UDP端口号
*/
public static final int PORT = 30000;
/**
* 每个数据报最大位4k
*/
public static final int DATA_LEN = 4096;
/**
* 接收网络数据的字节数组
*/
private byte[] buffIn = new byte[DATA_LEN];
/**
* 以指定字节数组创建准备接受数据的DatagramPacket
*/
private DatagramPacket packetIn = new DatagramPacket(buffIn, buffIn.length);
/**
* 定义一个用于发送的DatagramPacket对象
*/
private DatagramPacket packetOut;
public void start() {
try {
DatagramSocket socket = new DatagramSocket(PORT);
// 客户端输入的英文单词
String key = null;
// 服务端翻译的中文内容
String value = null;
// 获取客户端对象,通过该对象将数据写给对方,否则没有目的地
SocketAddress address = null;
// 返回给客户端的数据
byte[] receiveData = null;
System.out.println("英译汉服务器启动了");
while (true){
// 读取socket中的数据,然后将数据封装到 packet_in 中
socket.receive(packetIn);
// 获取客户端输入的数据
buffIn = packetIn.getData();
// 将byte数组转换为字符串 需要去掉后面的空格
key = new String(buffIn,0,buffIn.length).trim();
// 根据map的key获取value
value = maps.get(key);
if(null == value){
value = "默认值";
}
address = packetIn.getSocketAddress();
// 检查退出条件
if("down".equals(key)){
System.out.println("客户端输入的是:" + key);
receiveData = "服务器关闭了,请重试".getBytes();
packetOut = new DatagramPacket(receiveData, receiveData.length, address);
socket.send(packetOut);
break;
}else {
System.out.println("客户端输入的是:" + key + ",翻译后的结果是" + value);
// 构建服务端发送给客户端的数据
receiveData = ("你输入的英文单词翻译成中文是:" + value).getBytes();
// 构建 packetOut 发送数据
packetOut = new DatagramPacket(receiveData, receiveData.length, address);
socket.send(packetOut);
}
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器关闭了");
}
}
/**
* 定义map集合,用来存放需要翻译的单词。
*/
private static Map<String, String> maps = new HashMap<>();
// 向map中添加数据
static {
maps.put("dog", "狗");
maps.put("cat", "猫");
maps.put("fish", "鱼");
maps.put("bird", "鸟");
maps.put("pig", "猪");
}
public static void main(String[] args){
new UdpDictServer().start();
}
}
2.2 简单的TCP网络程序
TCP Socket API
ServerSocket
bind():
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要bind()绑定一个固定的网络地址和端口号。
- 如果地址null,则系统将接收临时端口和有效的本地地址来绑定套接字。
accept():
- 三次握手完成后,服务器调用accept()接受连接
- 如果服务器调用accept()时还没有客户端的请求连接,就阻塞等待直到有客户端连接上来
- Socket是一个返回值,代表网络的套接字
Socket类:
TCP通用服务器
TCP服务端,接收客户端内容然后输出
package frank.http;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Http 请求服务端.实现将客户端输入的英文单词翻译为中文。
*
* @author haozhang
* @date 2020/03/10
*/
public class HttpServer {
public void start() throws IOException {
ServerSocket server = new ServerSocket(8888);
System.out.println("服务器启动了");
//设置第一次输出[服务器启动了],启动后不再显示
boolean isShow = true;
while (true) {
try {
Socket client = server.accept();
String clientName = InetAddress.getLocalHost().toString();
if (isShow) {
System.out.println("客户端 " + clientName + " 已经连接到服务器了");
}
isShow = false;
BufferedReader br = new BufferedReader(
new InputStreamReader(client.getInputStream()));
String word = br.readLine();
if (word != null) {
System.out.println("客户端输入的是:" + word + " 服务器响应的是:" + word);
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(client.getOutputStream()));
bw.write(word + "/n");
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new HttpServer().start();
}
}
TCP客户端
package frank.http;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* Demo class
*
* @author haozhang
* @date 2020/03/10
*/
public class HttpClient {
public void start() {
try {
Socket socket = new Socket("127.0.0.1", 8888);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
BufferedReader br = new BufferedReader(new InputStreamReader(is));
Scanner scanner = new Scanner(System.in);
String key = null;
String val = null;
while (true) {
System.out.println("请输入数据:");
if (scanner.hasNext()) {
key = scanner.nextLine();
//向服务端发送一条信息
bw.write(key + "\n");
bw.flush();
//读取服务器返回的消息
val = br.readLine();
System.out.println("客户端的输入是:" + key + " 服务器的响应是:" + val);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new HttpClient().start();
}
}
英译汉服务器
package frank.http;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* HTTP 请求服务端.实现将客户端输入的英文单词翻译为中文。
*
* @author haozhang
* @date 2020/03/10
*/
public class HttpDictServer {
/**
* 定义map集合,用来存放需要翻译的单词。
*/
private static Map<String, String> MAP = new HashMap<>(16);
static {
MAP.put("dog", "狗");
MAP.put("cat", "猫");
MAP.put("fish", "鱼");
MAP.put("bird", "鸟");
MAP.put("pig", "猪");
}
public void start() throws IOException {
ServerSocket server = new ServerSocket(8888);
System.out.println("服务器启动了");
//设置第一次输出[服务器启动了],启动后不再显示
boolean isShow = true;
while (true) {
try {
Socket client = server.accept();
String clientName = InetAddress.getLocalHost().toString();
if (isShow) {
System.out.println("客户端 " + clientName + " 已经连接到服务器了");
}
isShow = false;
BufferedReader br = new BufferedReader(
new InputStreamReader(client.getInputStream()));
//key 客户端发送来的消息
String key = br.readLine();
//服务器翻译的结果
String val = MAP.get(key);
if (key != null) {
if (val == null) {
val = "default";
System.out.println("客户端的输入是:" + key + " 服务器的响应是:default");
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(client.getOutputStream()));
bw.write(val + "\n");
bw.flush();
} else {
System.out.println("客户端的输入是:" + key + " 服务器的响应是:" + val);
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(client.getOutputStream()));
bw.write(val + "\n");
bw.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new HttpDictServer().start();
}
}
测试多个连接的情况
再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求.
我们当前的这个TCP, 只能处理一个连接, 这是不科学的.
2.3 多线程版本网络程序
TCP 服务端
通过每个请求, 创建子进程的方式来支持多连接;
ServerThread 类为多线程类,StringServer类为调用类。
package frank.thread;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 多线程HTTP具体实现类
*
* @author haozhang
* @date 2020/03/10
*/
public class ServerThread implements Runnable {
private int index;
private Socket socket;
public ServerThread (Socket socket, int index) {
this.socket = socket;
this.index = index;
}
@Override
public void run() {
try {
try {
// 读取客户端传过来信息的DataInputStream
DataInputStream in = new DataInputStream(socket.getInputStream());
// 向客户端发送信息的DataOutputStream
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while (true) {
// 读取来自客户端的信息
String accept = in.readUTF();
System.out.println("第" + index + "个客户端发出消息:" + accept);
}
} finally {
// 建立连接失败的话不会执行 socket.close()
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package frank.thread;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 多线程HTTP版本的服务端
*
* @author haozhang
* @date 2020/03/10
*/
public class StringServer {
public void service() {
int i = 1;
try {
// 建立服务器连接,设定客户连接请求队列的长度
ServerSocket server = new ServerSocket(8080, 3);
while (true) {
if (i <= 3) {
// 等待客户连接
Socket socket = server.accept();
System.out.println("第" + i + "个客户连接成功!");
new Thread(new ServerThread(socket, i)).start();
i++;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new StringServer().service();
}
}
TCP 客户端
package frank.thread;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Random;
import java.util.Scanner;
/**
* 多线程HTTP版本的客户端
*
* @author haozhang
* @date 2020/03/10
*/
public class StringClient {
/**
* 客户端的名字
*/
private String name = String.valueOf(new Random().nextInt(999999));
public void chat() {
try {
// 连接到服务器
Socket socket = new Socket("localhost", 8080);
// 读取服务器端传过来信息的DataInputStream
DataInputStream in = new DataInputStream(socket.getInputStream());
// 向服务器端发送信息的DataOutputStream
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
// 装饰标准输入流,用于从控制台输入
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入数据");
String send = scanner.nextLine();
// 把从控制台得到的信息传送给服务器
out.writeUTF("客户端[" + name + "]:" + send);
// 读取来自服务器的信息
String accept = in.readUTF();
System.out.println(accept);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new StringClient().chat();
}
}
2.4 线程池版本的TCP服务器
TCP 服务端
package frank.threadpool;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Demo class
*
* @author haozhang
* @date 2020/03/10
*/
public class ThreadPoolServer {
public static void main(String[] args) throws IOException {
new ThreadPoolServer().run();
}
private void run() throws IOException {
ServerSocket server = new ServerSocket(9999);
ExecutorService executorService = Executors.newFixedThreadPool(10);
while (true) {
Socket client = server.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(this.getClass().toString());
execute(client);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 服务器运行代码
*/
public void execute(Socket client) throws IOException {
String clientName = InetAddress.getLocalHost().toString();
System.out.println("客户端:" + clientName + "已连接到服务器");
BufferedReader br = new BufferedReader(new
InputStreamReader(client.getInputStream()));
BufferedWriter bw = new BufferedWriter(new
OutputStreamWriter(client.getOutputStream()));
// key 客户端发送来的消息
String key = null;
// 服务器翻译后的结果
String value = null;
while (true) {
try {
key = br.readLine();
value = MAP.get(key);
if (null != key) {
if (null == value) {
value = "default";
System.out.println("客户端输入的是:" + key + ",服务端返回的是默认值:" +
value);
} else {
System.out.println("客户端输入的是:" + key + ",服务器翻译后是:" + value);
}
bw.write(value + "\n");
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 定义map集合,用来存放需要翻译的单词。
*/
private static Map<String, String> MAP = new HashMap<>();
// 向map中添加数据
static {
MAP.put("dog", "狗");
MAP.put("cat", "猫");
MAP.put("fish", "鱼");
MAP.put("bird", "鸟");
MAP.put("pig", "猪");
}
}
TCP 客户端
package frank.threadpool;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* Demo class
*
* @author haozhang
* @date 2020/03/10
*/
public class ThreadPollClient {
public static void main(String[] args) {
try {
Socket s = new Socket("127.0.0.1",9999);
//构建IO
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
Scanner scanner = new Scanner(System.in);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 输入的单词
String key = null;
// 单词的翻译结果
String value = null;
while (true){
System.out.println("请输入数据");
if(scanner.hasNext()) {
key = scanner.nextLine();
//向服务器端发送一条消息
bw.write(key+"\n");
bw.flush();
//读取服务器返回的消息
value = br.readLine();
System.out.println("客户端输入的是:"+key+",服务器翻译后是:"+value);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
线程池优点
- 利用线程池管理并复用线程、控制最大并发数等。
- 实现任务线程队列缓存策略和拒绝机制。
- 实现某些与时间相关的功能,如定时执行、周期执行等。
- 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免个服务线程互相影响。
线程池缺点
- 前期需要创建多个线程示例对象。
- 如果客户端连接少,会造成线程资源浪费。