目录
[1>:TCP协议 与 UDP协议](#1>:TCP协议 与 UDP协议)
(1).连接:通信双方分别存储了对方的关键信息(IP与端口号),可以建立起通信
[(2).可靠传输 与 非可靠传输](#(2).可靠传输 与 非可靠传输)
[(3).面向字节流 与面向数据报](#(3).面向字节流 与面向数据报)
[1.我们可以操控socket对象 进而控制网卡进行网络传输](#1.我们可以操控socket对象 进而控制网卡进行网络传输)
一.套接字(socket)
1.前提知识:
(1).IP与端口号
IP地址 :⽤于定位主机的⽹络地址,简单来说,就是看应用运行在哪台电脑上
端口号:标识服务器对应进程的位置,简单来说,就是看应用在电脑的哪个位置
(2).TCP/IP五层协议:

2.概念:
(1).简单认识
套接字是一组专门用于网络编程的一组类和方法
应用层 传送消息到传输层 ,需要调用对应的函数 ,而这些函数由操作系统提供 且由c实现,把这些方法称为套接字(socket) ,为了方便使用,Java将其封装为一组类和对象
(2).socket对象
硬盘用于存储数据 ,而为了方便观察到硬盘使用情况,采用文件的大小 来表示硬盘的使用
而网卡用于传输数据 ,为了方便控制传输内容,我们可以用socket对象来控制网卡来进行数据传输
3.传输层两种重要协议
注:这里只是简单了解,方便后续编程,具体会在新章节讲到
1>:TCP协议 与 UDP协议
TCP与UCP为了网络编程,提供了两组套接字,也就是接下来学习的目标,但在此之前先了解协议特点
2>.特点
TCP: 有连接,可靠传输,面向字节流,全双工
UDP:无需 连接,不可靠传输,面向数据报,全双工
(1).连接 :通信双方分别存储了对方的关键信息(IP与端口号),可以建立起通信
比如:两个认识的人打电话,如何确定对方身份呢?
TCP的做法(有连接) :双方手机互相存储对应的手机号与名字,拨打时看来电显示就可以确定身份,接着接通电话建立连接;删除连接 ,通信双方互相删除对应的关键信息 ,手机上则是按下挂断进行连接的释放
**UDP做法(无连接):**直接拿手机号给你发短信问你是不是那个人,至于对方回复也好,不回复也无所谓,不关心
(2).可靠传输 与 非可靠传输
首先要知道信息 在传输时可能会丢失数据 ,也就是丢包
TCP在传输时 会尽可能的传输数据,防止出现丢包现象,但速度稍微慢点 ,属于可靠传输
UDP在传输时 ,会以尽可能快 把数据传递,坏处就是出现丢包 现象,属于不可靠传输
(3).面向字节流 与面向数据报
面向字节流:指数据像水流一样连续传递
面向数据报 :明确要传递的内容信息 ,对方的IP地址与端口号,将这些内容打包为数据报进行传递
(4).全双工
全双工就是支持双方一起通信,不过协议不同会导致细微差距
以打电话为例:TCP全双工就是通信双方同一时间可以同时进行说话或听对方讲话
以发短信为例:UDP全双工就是同一时间,只能有一个人进行消息发送
4.UDP套接字编程
1>:介绍:
DatagramSocket:是UDP类型的socket对象 ,⽤于发送和接收UDP数据报
DatagramPacket:是UDP数据传输的基本单位
2>:方法介绍


因为方法不好单独演示,所以写一个程序来说明
3>:回显服务器(echo-server)
回显服务器相当于网络编程中的hello word,需要写客户端 与服务器 ,其功能是客户端向服务器发送请求,并打印服务器传回来的响应
请求:客户端向服务器发送的数据
响应:服务器向客户端发送的数据
(1).代码演示
服务器代码:
java
//服务器
public class EchoServer {
// 创建socket对象方便传递读取数据
private DatagramSocket socket ;
// 服务器运行在本机上,IP地址也在本机,且由操作系统处理
// 所以创建服务器时指定服务器的端口号(0-65535)
// 一般不考虑 0-1023,因为是操作系统留给其他重要服务的端口号
// 除此之外随便指定,若雨其他进程冲突,则会报错,此时再修改即可
public EchoServer(int port) throws SocketException {
// 初始化socket对象,并指定端口号 port
socket = new DatagramSocket(port);
}
/**
* 对于服务器,核心模版为:
* 1.读取请求并解析
* 2.根据请求计算响应
* 3.把响应传递给客户端
*/
public void start() throws IOException {
System.out.println("server begin");
// 因为不清楚客户端何时发消息,为了确保响应及时,采用 while 循环及时读取信息
while(true){
// 1.读取请求并解析
// 创建 DatagramPacket对象存储从socket读取的数据
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
// 从socket读取取客户端的请求到 datagramPacket 里
socket.receive(requestPacket);
// 此时 datagramPacket 存储的为字节数组,而客户端与服务器最好用字符串
// 将字节数组从 0 线标开始将1024个元素转为字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
// 2.根据请求计算响应
String response = process(request);
// 3.把响应传递给客户端
// 把响应传递给客户端,需要知道: 传递内容 目标IP地址与端口号
// 所以构造 DatagramPacket 数据报对象存储这些
// 数据报对象包括:字节数组 数组长度 客户端IP与端口号
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4.打印一下日志
// 客户端IP 端口号 请求 响应
System.out.printf("[%s:%d], request: %s response: %s \n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
/**
* 将请求通过 process 方法计算响应
* 不过此时,进行最简单的处理
*/
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
EchoServer echoServer = new EchoServer(9909);
echoServer.start();
}
}
客户端代码:
java
// 客户端
public class Echo_client {
// 创建socket对象方便传递读取数据
private DatagramSocket socket;
// 存储服务器的 IP 与 端口号
private String serverIp;
private int serverPort;
// 首先客户端要向服务器发送信息,需要存储对应的 IP 与 端口号
// 我们希期望初始化客户端时就清楚这些,所以构造方法传递对应参数并存储
// 其次,服务器初始化时给定端口号,由程序员操作,所以端口号不会发生冲突
// 对于客户端来说不需要指定端口号,因为无法保证指定的与用户的某个程序的端口号相同从而发生冲突
public Echo_client(String serverIp,int serverPort) throws SocketException {
// 不指定不代表不需要,采用无参构造方法随机指定一个空闲的端口号
socket = new DatagramSocket();
// 保存服务器IP与端口号
this.serverIp = serverIp;
this.serverPort = serverPort;
}
// 此时五元组已确定,即:协议:UDP协议 源IP:本机 源端口号:随机指定的 目标IP:serverIp 目标端口号:serverPort
// 可以开始传输数据
/**
* 客户端需要做从控制台把字符串发送给服务器,从中读取响应
* 核心模版为:
* 1.读取字符串
* 2.构造UDP数据报,发送请求
* 3.读取响应
*/
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
// 方便多次发送请求
while(true){
// 1.读取字符串
System.out.print("请输入请求内容>:");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("程序结束");
break;
}
// 2.构造UDP数据报,发送请求
// InetAddress.getByName() 获得目标IP
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIp),this.serverPort);
socket.send(requestPacket);
// 3.读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
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 {
// 给定服务器的IP与端口号
Echo_client echo_client = new Echo_client("127.0.0.1",9909);
echo_client.start();
}
}
(2).演示:

4>:简单翻译服务器
因为服务器的模版都是一样,所以只需继承回显服务器在扩招其他功能即可
java
public class TranEchoServer extends EchoServer {
// 存储 英文与翻译键值对
private static Map<String,String> translate = new HashMap<String,String>();;
static {
translate.put("香蕉","banana");
translate.put("苹果","apple");
translate.put("你好","hello");
translate.put("世界","word");
}
// 这里不需要给当前翻译服务器端口号,因为它本质是调用回显服务器进行数据传输
public TranEchoServer(int port) throws SocketException {
super(port);
}
// 这里 读取响应等操作都是一样,所以只需要重写 "请求计算响应" 这个步骤
@Override
public String process(String request){
return translate.getOrDefault(request,"没有该值翻译");
}
public static void main(String[] args) throws IOException {
TranEchoServer tranEchoServer = new TranEchoServer(10000);
tranEchoServer.start();
}
}

5.TCP套接字编程
TCP涉及到两种网络模型:
短连接:一个连接,只发送一个响应就关闭
长连接:一个连接,进行多次发送响应后才关闭
1>:介绍
ServerSocket (服务套接字):专门根据客户端的访问 返回一个socket对象
Socket :负责数据传输与接受
2>:方法介绍


3>:回显服务器
(1).代码
服务器代码:
java
public class TCPEcho_server {
// 创建 serversocket 对象,用于与客户端建立连接返回socket对象
private static ServerSocket serverSocket ;
// 给定服务器端口号
public TCPEcho_server(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("server begin");
while(true){
// 首先先与客户端进行连接,才能进行后续操作
// 此时 socket对象草扩双方的IP与端口号
Socket socket = serverSocket.accept();
// 接着进行后续连接操作
processconnection(socket);
}
}
private static void processconnection(Socket socket) {
// 1.服务器要能哪个客户端进行访问,打印日志
System.out.printf("[%s : %d]客户端上线\n",socket.getInetAddress().toString(),socket.getPort());
// 2.获得流对象进行数据交换 读取客户端的请求与发送响应
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
// //此处读取麻烦了,选择采用更加优秀的做法
// while(true){
// byte[] bytes = new byte[1024];
// int n = inputStream.read(bytes);
// if(n == -1)break;
// }
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
// 处理多个请求
while(true){
// 3.读取请求
// 此处采取短连接,当服务器没有读到值,默认本次链接结束
if(!scanner.hasNext()){
// 代表读取完了
System.out.printf("[%s : %d]客户端关闭请求\n",socket.getInetAddress().toString(),socket.getPort());
break;
}
String request = scanner.next();
// 4.根据请求计算响应
String response = process(request);
// 5.发送响应
printWriter.println(response);
// 6.打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
}catch (IOException e){
e.printStackTrace();
}
}
private static String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEcho_server tcpEcho_server = new TCPEcho_server(12345);
tcpEcho_server.start();
}
}
客户端代码:
java
public class TCPEcho_client {
// 创建 socket 对象用于进行数据交流
public Socket socket = null;
// 当 socket 对象赋值时会根据所传输的服务器IP与端口号自动创建连接
// 给定所连接的服务器的IP与端口号
public TCPEcho_client(String IP,int port) throws IOException {
socket = new Socket(IP,port);
}
public void start(){
Scanner scanner = new Scanner(System.in);
// 创建流对象进行数据传输
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerNewWork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while(true){
// 1.从控制台读取数据
System.out.printf("输入数据>:");
String request = scanner.next();
// 2.发送请求
writer.println(request);
// 3.读取响应
if(!scannerNewWork.hasNext()){
break;
}
String response = scannerNewWork.next();
// 4.把响应展示在控制台上
System.out.println(response);
}
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEcho_client tcpEcho_client = new TCPEcho_client("127.0.0.1",12345);
tcpEcho_client.start();
}
}
(2)运行截图

(3).问题:
a.缓冲区问题
对于TCP协议而言,内置了一个缓冲区 ,当我们进行写入操作时,会先把数据放入缓冲区中,等满了才会读取 ,为了保证数据的快速读取,提供了flush()冲缓冲区方法,会把当前缓冲区数据冲出使其被可读取
对于UDP而言,也有缓冲区,不过在内核里面,应从程序用不到
此时在代码中加入


此时代码就正确了
b.内存溢出问题
对于socket来说,也是个文件,使用时要消耗资源 ,而服务器的serversocket 与客户端的socket的生命周期随进程结束而结束,这个不用管,但注意:在服务器accept返回的socket对象还没关闭呢

此时只要客户端退出,直接close()关闭socket对象与连接
c.一个服务器对多个客户端
当我们创建多个客户端

可以发现,只有客户端1建立连接了,客户端二不管进行怎么操作在服务器都没显示,这是不正确的
原因 : 创建的客户端在构造方法处就会与服务器建立联系
当客户端1建立联系后,就会与服务器进行数据传输,此时服务器在等待客户端1输入数据,而其他客户端因为服务器在忙碌此时无法建立联系,自然就会在accept()处阻塞,等待服务器空闲时再建立联系

解决方法:
1.加入多线程,让每个线程负责一个客户端
java
public void start() throws IOException {
System.out.println("server begin");
while(true){
Socket socket = serverSocket.accept();
// 每次建立连接都的socket,都会分配线程执行当前连接
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
processconnection(socket);
}
});
thread.start();
}
}
每个线程发送"我是客户端X",可以清晰看到都可以与服务器进行连接

此时结构图为

2.引入线程池: 当客户端多了,线程的创建与销毁都伴随着大量的资源消耗,若是可以提前把线程创建好,需要时直接来取,可以明显减少资源消耗
java
public void start() throws IOException {
System.out.println("server begin");
// 不能使用 FixedThreadPool,线程数目固定
ExecutorService executorService = Executors.newCachedThreadPool();
while(true){
Socket socket = serverSocket.accept();
executorService.submit(()->processconnection(socket));
}
}
3.文件IO多路复用:一个线程负责多个客户端
对于线程池来说,线程数量也是有上线的,所以线程池一般负责短连接,但进入长连接后,每个线程长时间运行,很快就会达到最大线程数量,所以引入文件IO多路复用,让一个线程负责多个客户端,极大缓解这种情况

6.发送请求的注意

对这个代码,采用next() 读取请求,逻辑为:遇到空白符停止读取并返回,那么发送时要使用如图的方法,自动加\n,否则就对方无法正常读取
二.总结
1.我们可以操控socket对象 进而控制网卡进行网络传输
2.对于TCP而言
(1).需要通过accept返回连接后的socket对象 ,通过该对象调用InputStreasm OutputStream流对象进行数据传输
(2).传输数据其实传输到缓冲区 ,要用**flush()**进行冲刷操作读取数据
(3).对于accept返回的socket对象要及时close()释放 ,防止文件描述表满了无法打开文件
(4).一个服务器对多个客户端需要引入多线程
(5).发送请求或响应注意用 println(),方便后续读取
3.对于UDP而言
通过数据报 进行数据传输,需要注意数据报传输时要保存对方的IP与端口号
这里只是简单的编程学习,为后续网络编程打基础,如果对你有帮助,点一个吧~~~