前言:花了一个多星期的时间,重新学习了计算机网络中可靠数据传输的知识和TCP、UDP协议。在掌握了基本的理论后,想通过编程实践,来巩固和加深下对Socket通信的理解。
操作系统:win10
Java版本:1.8
开发工具:IDEA 2021
本项目基于TCP协议,实现了客户端和服务器的Socket通信。项目主要实现了查字的功能,客户端输入要查询的一个汉字,然后回车发送到服务器端,服务器把汉字的查询结果在返回给客户端。另外,在使用服务器提供的字典服务前,客户端需要先进行登录。
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。它将两个端系统(或主机)间IP的交付服务扩展到了运行在端系统上的两个进程之间的交付服务。举个容易理解的真实例子,我们学校说是为了安全不允许外卖员把外卖送到寝室里,并且我们假设食堂的各家餐厅生产的外卖有人负责把它们拿到楼下的统一地点。外卖小哥(IP协议)根据外卖地址(IP地址)负责把外卖拿到我们住的宿舍楼下(目的主机),宿管阿姨(TCP协议)根据姓名(端口号)负责把外卖分发到具体的学生(进程)手里
多路分解:将运输层的报文段中的数据交付到正确的套接字的工作称为多路分解,之后应用层的进程会从它绑定的套接字中获取数据。
多路复用:在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(主要是端口号,这将用于分解)从而生成报文段,然后将报文段传递到网络,这些工作称为多路复用。
从某方面来说:IP是主机的标识,端口号是进程的标识
TCP连接是两个端系统(源主机和目的主机)中的状态信息。位于因特网核心的网路元素(路由器和链路层交换机)只运行IP协议,TCP只运行在端系统中,因此TCP连接一条逻辑的连接,只是两个端系统中的状态信息,即源主机知道目的主机的存在,目的主机也知道源主机的存在。
TCP连接的组成包括:一台主机上的缓存(数据缓冲区)、变量(序号、窗口等信息)和与进程连接的套接字,以及另一台主机上的缓存、变量和套接字。
关于为什么是三次握手建立TCP连接及序号的重要性,有些博文介绍的已经什么透彻了,不要在这里再次叙述,推荐TCP 为什么三次握手而不是两次握手(正解版)
1.TCP服务器端,选择了一个端口号,如8080,开启了一个进程,来“监听”此端口,等待来自TCP客户端的连接建立请求。此时的套接字标识为【0.0.0.0:0,目的主机ip:8080】,此套接字称为“欢迎套接字”,用于建立TCP连接,并不用于数据传输
2.TCP客户端进程创建了一个套接字发送了一个连接建立请求报文段(SYN置为1,还包含客户端为进程选择的端口号如10000)
3.TCP服务端进程监听到客户端的连接建立请求,创建子进程(或线程)和套接字,称为“连接套接字”,用于和客户端的数据传输。
此时TCP连接已经建立,后续有新的连接建立请求,则重复上述过程。如果TCP发送数据,则会根据检查套接字,根据套接字把数据交付给对应的进程,即多路分解。
package com.gzn.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Main {
private static class ServerMsg{
public static final String WELCOMING = "欢迎使用《明月几时有字典》,请按照提示操作。注:回车后可提交信息,查询汉字为“#”表示退出!";
public static final String INPUT_USERNAME = "请输入用户名:";
public static final String INPUT_PASSWORD = "请输入密码:";
public static final String LOGIN_SUCCEED_CODE = "1001";
public static final String LOGIN_FAIL_CODE = "1002";
public static final String NOT_FOUND_CODE = "2001";
}
// 用户列表
private static Map<String, String> users;
private static Map<String, String> dict;
// 初始化用户列表和字典
private static void init(){
// 初始化用户
users = new HashMap<>();
users.put("admin", "123123");
// 初始化字典
dict = new HashMap<>();
dict.put("明", "【读音:ming】" + "组词:明天、明亮");
dict.put("月", "【读音:yue】" + "组词:月亮、月光");
dict.put("几", "【读音:ji】" + "组词:几个、几乎");
dict.put("时", "【读音:shi】" + "组词:时间、小时");
dict.put("有", "【读音:you】" + "组词:没有");
}
private static class Worker implements Runnable{
private Socket connectionSocket;
private PrintWriter sendBuffer;
private BufferedReader recvBuffer;
public Worker(Socket connectionSocket) throws IOException {
this.connectionSocket = connectionSocket;
sendBuffer = new PrintWriter(new OutputStreamWriter(connectionSocket.getOutputStream(), "UTF-8"), true);
recvBuffer = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream(), "UTF-8"));
}
private void welcoming() throws IOException {
send(ServerMsg.WELCOMING);
}
private void login() throws IOException {
boolean flag = false;
while(!flag){
send(ServerMsg.INPUT_USERNAME);
String username = receive();
send(ServerMsg.INPUT_PASSWORD);
String password = receive();
if (users.get(username) != null && users.get(username).equals(password)){
send(ServerMsg.LOGIN_SUCCEED_CODE);
flag = true;
}else {
send(ServerMsg.LOGIN_FAIL_CODE);
}
}
}
private boolean contains(String word){
return dict.containsKey(word);
}
private String lookup(String word) throws IOException {
return dict.get(word);
}
/**
* 向套接字的发送缓存写入信息
* @throws IOException
*/
private void send(String msg) throws IOException{
sendBuffer.println(msg);
}
/**
* 从套接字的接收缓存读取信息
* @return
* @throws IOException
*/
private String receive() throws IOException{
return recvBuffer.readLine();
}
@Override
public void run() {
try {
// 连接成功发送欢迎信息
welcoming();
// 登陆
login();
// 查词服务
String word = receive();
while(word != "#") {
System.out.println(word);
if (contains(word))
send(lookup(word));
else
send(ServerMsg.NOT_FOUND_CODE);
word = receive();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
sendBuffer.close();
recvBuffer.close();
connectionSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String args[]) {
// 初始化
init();
try{
// 创建监听套接字
ServerSocket serverSocket = new ServerSocket(8888);
// 监听端口:“10000”,等待客户端发起连接,TCP状态:LISTEN
// 连接套接字负责与客户端通信
while(true){
Socket connectionSocket = serverSocket.accept();
new Thread(new Worker(connectionSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端通信需要的核心对象:
ServerSocket “欢迎套接字”,监听连接建立请求,创建负责通信的“连接套接字”;
Socket (通过serverSocket.accept()获取)“连接套接字”,负责与客户端通信;
InputStream (通过connectionSocket.getInputStream()获取),字节输入流,其输入源为服务器端套接字的接收缓存;
OutputStream (通过connectionSocket.getOutputStream()获取),字节输出流,其输出源为服务器端套接字的发送缓存;
package com.gzn.client;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class Main {
private static class ClientMsg{
public static final String LOGIN_SUCCEED_CODE = "1001";
public static final String LOGIN_SUCCEED = "登陆成功,您现在可以使用查字服务了。";
public static final String LOGIN_FAIL = "登陆失败,用户名或者密码错误。";
public static final String NOT_FOUND_CODE = "2001";
public static final String NOT_FOUND = "对不起,您查找的汉字尚未收录本字典";
}
private static final String hostname = "127.0.0.8";
private static final int port = 8888;
private static final int requestTimeout = 60*1000;
private static Socket clientSocket;
private static BufferedReader recvBuffer;
private static PrintWriter sendBuffer ;
static {
try {
clientSocket = new Socket();
clientSocket.connect(new InetSocketAddress(hostname, port), requestTimeout);
recvBuffer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
sendBuffer = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 向套接字的发送缓存写入信息
* @throws IOException
*/
public static void send(String msg) throws IOException{
sendBuffer.println(msg);
}
/**
* 从套接字的接收缓存读取信息
* @return
* @throws IOException
*/
public static String receive() throws IOException{
return recvBuffer.readLine();
}
public static void main(String[] args) {
try{
// 欢迎信息
System.out.println(receive());
// 登陆信息
Scanner scanner = new Scanner(System.in);
boolean flag = false;
while(!flag){
System.out.println(receive());
String username = scanner.nextLine();
send(username);
System.out.println(receive());
String password = scanner.nextLine();
send(password);
String result = receive();
if(result.equals(ClientMsg.LOGIN_SUCCEED_CODE)){
System.out.println(ClientMsg.LOGIN_SUCCEED);
flag = true;
}else {
System.out.println(ClientMsg.LOGIN_FAIL);
}
}
// 查字
System.out.println("请输入您要查询的汉字:");
String word = scanner.nextLine();
while(!word.equals("#")){
if(word.length() != 1){
System.out.println("您的输入为空或者多于一个汉字!");
}else{
// 查词
send(word);
String result = receive();
if (result.equals(ClientMsg.NOT_FOUND_CODE)){
System.out.println(ClientMsg.NOT_FOUND);
}else{
System.out.println(result);
}
System.out.println("--------------------------------");
}
System.out.println("请输入您要查询的汉字:");
word = scanner.nextLine();
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
System.out.println("欢迎下次使用!");
sendBuffer.close();
recvBuffer.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
客户端核心对象(和服务器端差不多):
Socket (通过手动new)“客户套接字”,负责与客户端通信;
InputStream (通过clientSocket.getInputStream()获取),字节输入流,其输入源为服务器端套接字的接收缓存;
OutputStream (通过clientSocket.getOutputStream()获取),字节输出流,其输出源为服务器端套接字的发送缓存;