基于TCP/IP的Java网络编程


这学期学的计算机网络这门课 发现确实挺有意思 看完书和试验觉得看Java寄出的时候没有去细看网络编程这一部分,现在了解了网络知识之后回头来补这段java基础
本文主要是想记录一下初学计算机网络这门课的一些认识以及写一些用Java语言基于TCP/IP协议编程的实际操作中遇到的一些小坑
主要是Java进行编写客户服务器之间的通信 对网络基础只是简单的进行描述 并不会长篇大论
初学 理解不深 只是简单的进行了一个小的实验 帮助自己理解 以后再补充

计算机网络基础

首先说一下以太网最基本最重要的知识——OSI参考模型

OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互联模型。该体系结构标准定义了网络互联的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层和应用层),即OSI开放系统互连参考模型。在这一框架下进一步详细规定了每一层的功能,以实现开放系统环境中的互连性、互操作性和应用的可移植性

简单来说就是推荐所有公司使用这个规范来控制网络,这样所有公司都有相同的规范,就能互联了
分七层从下到上分别是:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层
但是由于这个模型太理想化 而我们在大多数情况下使用的都是TCP/IP五层模型:
应用层 传输层 网络层 链路层 物理层
而每一层都有他的主要功能和代表协议 此处不再细说 略见下图:
TCP/IP

而此处的Java网络编程(也叫Socket编程)就是使用位于传输层的TCP或者UDP协议进行的
之后我们来主要说一下这两个协议

TCP & UDP

两者都是位于传输层的协议
面向连接的TCP(Transmission Control Protocol,传输控制协议)

1,可靠性:它是基于连接的协议,在正式收发数据前,必须和对方建立可靠的连接。(三次握手)
2,拥有拥塞控制 流量控制
3,不提供时间 最小吞吐量保证 安全性保证等
4,适用于传输要求高的
5,TCP进行通信的两个应用进程 客户端服务端

面向非连接的UDP协议

1,不可靠:不需要建立传输连接
2,没有拥塞控制,流量控制 吞吐量保证
3,发送数据结束时无需释放资源 速度快
4,适用于要求不高的少量数据
5,每个数据报大小限制在64K之内

三次握手

TCP在传输之前会进行三次沟通,一般称为“三次握手”,
传完数据断开的时候要进行四次沟通,一般称为“四次挥手”
此处先只说一下”三次握手”

先形象化的说一下:

在晚上灯光暗的时候 你看见一个女生迎面走来 你不确定人不认识她:
你首先向妹子招手(syn),妹子看到你向自己招手后,向你点了点头挤出了一个微笑(ack)。你看到妹子微笑后确认了妹子成功辨认出了自己(进入estalished状态)。

但是妹子有点不好意思,向四周看了一看,有没有可能你是在看别人呢,她也需要确认一下。妹子也向你招了招手(syn),你看到妹子向自己招手后知道对方是在寻求自己的确认,于是也点了点头挤出了微笑(ack),妹子看到对方的微笑后确认了你就是在向自己打招呼(进入established状态)。两个人确认是熟人
于是你们打招呼成功。

过程分四个动作:

1,你招手
2,女生点头微笑
3,女生向你招手
4,你点头微笑

2和3合并成一个动作 就形成了三次握手的形象化 其实本质上 三次握手第二次就是一个两个动作的整合 下面我们说一下专业化的语言

三次握手:
第一次:客户机给服务器发送请求连接(此时客户端不知道请求是否发送成功)
第二次:服务器给客户端反馈答复(服务端接收到 客户端发送成功进行反馈 服务器不知道自己是否发送成功)
第三次:客户端在给服务端反馈(双方都确认可以进行发送和接收)表示可以建立连接
需要用到的概念
两个序号:

(1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记
(2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。

两个标志位:

(A)ACK:确认序号有效。
(B)SYN:发起一个新连接。

需要注意的是:
(A)不要将确认序号ack与标志位中的ACK搞混了。
(B)确认方ack=发起方req+1,两端配对。

主要过程:

(1)第一次握手:Client将标志位SYN置为1,
随机产生一个值seq=X(就是客户端的数据初始地址) 并将该数据包发送给Server Client进入SYN_SENT状态 等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=X+1,随机产生一个值seq=Y(就是服务器端划分出的存储初始地址),并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为X+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=Y+1,并将该数据包发送给Server,Server检查ack是否为Y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

过程如下图所示:
Handshake

Socket

IP地址和端口号 组成的套接字
利用套接字开发应用程序被广泛使用 通信的两端都要有Socket
Socket允许程序把网络连接当成一个流 数据可以在两个Socket之间通过IO进行传输
一般主动发起通信的应用程序是客户端 等待通信请求的是服务端

网络编程实际上就是Socket编程

Socket编程实例

使用Socket进行基于TCP的简单编程
原理与IO流输入输出一致

客户端给服务端发送消息 服务端输出信息到控制台上
服务器端收到信息后给客户端进行反馈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class TestTCP {
//客户端
@Test
public void client(){
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
try {
socket = new Socket(InetAddress.getByName("127.0.0.1"),8989);
outputStream = socket.getOutputStream();
outputStream.write("I'm Client".getBytes());

socket.shutdownOutput();
inputStream = socket.getInputStream();
byte[] b= new byte[20];
int len;
while ((len=inputStream.read(b))!=-1){
String s = new String(b,0,len);
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务器端
@Test
public void server(){
ServerSocket serverSocket = null;
Socket socket = null;
InputStream inputStream = null;
OutputStream outputStream =null;
try {
serverSocket = new ServerSocket(8989);
socket = serverSocket.accept();
inputStream = socket.getInputStream();

byte[] b = new byte[20];
int len;
while((len = inputStream.read(b))!=-1){
String s = new String(b,0,len);
System.out.println(s);
}
outputStream=socket.getOutputStream();
outputStream.write("I have recieve your message".getBytes());
} catch (IOException e) {
e.printStackTrace();
}finally {
if (outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

执行时先启动服务器端 再启动客户端
服务器端启动后无任何反馈 等待客户端启动 客户端启动后 两端的显示如下:

服务器端:
TCP/IP

客户端:
TCP/IP

注意(排坑)

一:
如程序中所写Socket由IP地址和端口号两部分组成

Socket=IP地址+端口号

二:

因为read()方式是堵塞式的:没有东西进行输入的话就会处于等待

服务器端用read()接受
但是服务端不知道客户端什么时候发送完数据 就一直处于等待 处于堵塞状态
下方程序因此无法进行 所以如果发送完数据的话 应该告诉服务端:我已经发送完毕
类似于三次握手确认阶段的实现

我第一次写的时候因为没有注意到这一点 导致服务器端接收到了第一次客户端发送到字段
但是一直处于运行状态 而客户端没有数据显示

此时需要在发送方写入方法之后加上一行

1
socket.shutdownOutput();

用来告诉接收端:”我已经发送完毕” 可以开始接收

三:
使用完的Socket IO流等记得在程序中加上关闭语句 防止机器资源浪费



写于计网期中考试完毕的一个反思夜晚 以后再进行补充