在实现具体代码前,我们先来简单了解下TCP/UDP协议
TCP在OSI模型中位于传输层在网络层之上,故在端到端传输的基础上将数据以端口号等标识实现进程/终端设备应用的区分,将数据精准的传达。
TCP全称为传输控制协议具有以下特点:
- 面向有连接的服务可靠的数据传输,即在通信前需建立连接进行一系列特定指令
- 流量控制:对流量进行监视控制,以接收方的接收窗口反馈而确认
- 拥塞控制:监视信道,当信道/带宽占用率升高时,限制数据的发送速度,以拥塞窗口反馈信息决策
- TCP的报文格式:每行总长度32bit 选项解释
- 接收窗口:用于判断接收端的数据接收状态,即流量控制,共占用16bit
- 确认号和序号:使得报文序列有序,于接收端对报文的差错校验(报文丢失,序列不一时快速丢弃/重传,具有回退N步(Go-Back-N)与选择重传),序号则可对冗余数据分组进行辨别,以便更快的进行差错恢复而无需等待超时定时器的响应(这样会使得效率降低)
- 校验和:进行差错检验
简要概述一下可靠传输的流程(于流水线化下的可靠数据传输):
一报文划分多个分组,每个分组都有序号,当一个分组到达接收方若成功则返回ACK
确认报文(暂不考虑其他差错)。序号的范围和对缓冲的要求取决于数据传输协议将如何处理
丢失、损坏、乱序及较大分组时如何延时处理等操作,故而就有了回退N步于选择重传协议(简单说一下二者都使用到了滑动窗口协议,通过发送窗口与接收窗口的不同长度则有累积确认(go-back-n)与选择确认(selective-repeat))。
TCP面向连接的重要特性:拥塞控制与流量控制
- 拥塞控制:具有其指定的拥塞避免算法(通过发\收方具有的一些指示器变量反馈信息即检测信道占用等设计的一套算法,这里不再深入)
再来解释一下建立连接的三次握手及关闭连接的四次挥手:
- 三次握手建立连接:为保证连接的可靠性,首先客户端向服务端发送连接建立报文,后等待对方的ACK回应,再客户端发送ACK确认收到(此时双方都具有了发送并接收报文都成功事件,便可认为本次连接将是可靠的)
UDP协议的特点与通信机制:
- 此为面向无连接的传输层协议,即无需通信前建立连接,报文格式:
- 源端口与目的端口号:定位进程,通过udp数据包的形式封装。
- 校验和与长度:udp校验和进行差错检验(仅进行简单的差错对比,保证端到端原则),因不能保证数据在传输途中的链路都提供差错检验机制,为避免引入比特差错则需自身需有一校验机制,在数据封装前进行校验和计算将结果封装到该字段。长度则为数据报大小。
总体与tcp比较而言:udp无连接状态首部开销小,故无需维护连接,因此传输速率/延时也将更快(上限取决于链路带宽),延迟也就更低,也因无差错回复机制对与数据丢失udp将不进行其他操作(如实时多媒体,很多都以udp方式进行传输以保证数据的实时性,然对某一块数据的丢失则进行等待下一块数据的到来,也就将丢失此处信息)
TCP socket通信图示
TCPClient.java的主例程代码
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
BufferedReader readSentence=null;
DataOutputStream outToServer=null;
BufferedReader inFromServer=null;
try{
readSentence=new BufferedReader(new InputStreamReader(System.in));
Socket clientSocket=new Socket(InetAddress.getLocalHost(),12345);//客户端socket
outToServer=new DataOutputStream(clientSocket.getOutputStream());
String sentence=readSentence.readLine();
outToServer.writeBytes(sentence);
inFromServer=new BufferedReader(new
InputStreamReader(clientSocket.getInputStream()));
clientSocket.shutdownOutput();
String modifiedSentence=inFromServer.readLine();
System.out.println("Modified data is :"+modifiedSentence);
clientSocket.close();
inFromServer.close();
}catch (IOException e)
{
e.printStackTrace();
}finally {
try {
if(readSentence!=null)
{
readSentence.close();
}
}catch (IOException e){
e.printStackTrace();
}
try {
if(outToServer!=null)
{
outToServer.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
}
下面就对代码做些简要的解释
这里都使用到了io流,主要的java.net.Socket包为Socket套接字编程所依赖。
BufferedReader readSentence=null;
DataOutputStream outToServer=null;
BufferedReader inFromServer=null;
readSentence为一字符流这里将其置为null(抛异常使用try-catchf方式,也可直接throws)作为一输入缓冲流,接收键盘输入字符,outToServer则为Socket中获取的输出流(socket通信在stream-pipeline中交互)带着报文段打出去,inFromServer为Socket中的输入流接收服务端发来的报文。
readSentence=new BufferedReader(new InputStreamReader(System.in));
Socket clientSocket=new Socket(InetAddress.getLocalHost(),12345);
outToServer=new DataOutputStream(clientSocket.getOutputStream());
String sentence=readSentence.readLine();
InputStreamReader(System.in)获取键盘输入放入到该流中转为字符流(相应的OutputStreamWriter()将流转为字节流) 初始化字符缓冲流readSentence,clientSocket为客户端socket进程需包含服务端主机号及进程端口号(这里用到了InetAddress下的静态方法getLocalHost获取本地ip地址,也可使用InetAddress.getByname(String host)指定主机地址返回为InetAddress对象直接作为对方主机地址)这里使用localhost可行是因为Client与Server进程都在本地,物理网卡都为同一。outToServer由clientSocket中的输出流初始化作为将发至服务器端的字节流。sentence存储键盘中输入的字符。
outToServer.writeBytes(sentence);//将数据写入到socket-stream中
inFromServer=new BufferedReader(new
InputStreamReader(clientSocket.getInputStream()));//初始化输入流接收服务端数据
clientSocket.shutdownOutput();//暂停clientSocket下的输出流避免再此阻塞
String modifiedSentence=inFromServer.readLine();//接收服务端发来的数据
System.out.println("Modified data is :"+modifiedSentence);//打印发过来的数据
clientSocket.close();//关闭此socket
inFromServer.close();//此也可在finall中使用try-catch方式关闭
建立了socket客户端后获取该进程的输入输出流与服务端进行交互(对流的操作都因关闭)。
流程简要汇总
TCPServer.java代码例程
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
ServerSocket welcomeSocket=null;
BufferedReader readFromClient=null;
DataOutputStream outToClient=null;
try {
welcomeSocket=new ServerSocket(12345);
while (true){
Socket clientSocket=welcomeSocket.accept();
readFromClient=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String fromClientSentences=readFromClient.readLine();
clientSocket.shutdownInput();
outToClient=new DataOutputStream(clientSocket.getOutputStream());
System.out.println(fromClientSentences);
outToClient.writeBytes(fromClientSentences+"is ACK");
clientSocket.shutdownOutput();
clientSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(welcomeSocket!=null){
welcomeSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}
try {
if(readFromClient!=null){
readFromClient.close();
}
}catch (IOException e){
e.printStackTrace();
}
try {
if(outToClient!=null){
outToClient.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
}
大部分代码都与客户端相同,流程也类似,简单解释下不同点。
ServerSocket welcomeSocket=null;
BufferedReader readFromClient=null;
DataOutputStream outToClient=null;
这里的ServerSocket为连接前需保证可靠所需socket对象,及通信前进行三次握手,往后的两个流则作为接收与发送报文的流。
welcomeSocket=new ServerSocket(12345);
初始化该进程端口号,客户端端口号需与此保持一致即可连接。
Socket clientSocket=welcomeSocket.accept();
建立一客户端的socket,由ServerSocket下的accept()方法进行侦听客户端socket连接后作为Socket的对象即可与对方交互。往后代码的操作都为获取该连接后的socket对象中的输入输出流进行操作,输入输出与客户端应保持读写对应(该socket-stream为单向流,半双工形式)否则将无法进行数据通信。
UDP通信的代码例程。
UDLClient.java代码部分:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) {
BufferedReader inFromUser=null;
DatagramSocket clientSocket=null;
try{
inFromUser=new BufferedReader(new InputStreamReader(System.in));
InetAddress hostid=InetAddress.getLocalHost();
clientSocket=new DatagramSocket();
byte[] receiveSentence=new byte[1024];
String sentence=new String(inFromUser.readLine());
byte[] sendSentence=sentence.getBytes();
DatagramPacket sendPackt=new DatagramPacket(sendSentence,0,sendSentence.length,hostid,8991);
clientSocket.send(sendPackt);
DatagramPacket recvPacket=new DatagramPacket(receiveSentence,receiveSentence.length);
clientSocket.receive(recvPacket);
String modifiedSentence=new String(recvPacket.getData(),0,recvPacket.getLength());
System.out.println("Modified data is:"+modifiedSentence);
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(inFromUser!=null){
inFromUser.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
}
接下来从头至尾解释一下代码的基本含义:
BufferedReader inFromUser=new BufferedReader(new InputStreamReader(System.in));
DatagramSocket clientSocket=clientSocket=new DatagramSocket();;
这里依旧使用的了字符缓冲流来接收键盘的输入,而DatagramSocket则则作为使用UDP通信所需的主要api,正如该api名称数据报套接字(使用UDP协议通信传输的报文我们也称之为udp数据报)
InetAddress hostid=InetAddress.getLocalHost();
clientSocket=new DatagramSocket();
byte[] receiveSentence=new byte[1024];
String sentence=new String(inFromUser.readLine());
byte[] sendSentence=sentence.getBytes();
DatagramPacket sendPackt=new
DatagramPacket(sendSentence,0,sendSentence.length,hostid,8991);
clientSocket.send(sendPackt);
使用InetAddress获取或指定主机id,这里直接使用本地方式 ,而后建立两个字节型数组对象用来发送前与接收数据的存储(这里发送与接收的都为字节流,若sendSentence提取指定大小过大后封装进数据报时该数据报大小则为该字节数组的大小,进行数据打印时其余空间都默认为0,为了简便这里的大小直接以数据的大小而决定sentence.getBytes()),后续则建立一DatagramPacket对象使用其构造器初始化(DatagramPacket(databuffer,offset,end,hostid,port))使用数据报将数据封装指明对方的端口号及主机地址,最后使用DatagramSocket对象下的send方法将该数据报打出。
DatagramPacket recvPacket=new DatagramPacket(receiveSentence,receiveSentence.length);
clientSocket.receive(recvPacket);
String modifiedSentence=new String(recvPacket.getData(),0,recvPacket.getLength());
System.out.println("Modified data is:"+modifiedSentence);
clientSocket.close();
与发送时相似,再次建立一数据报对象作为接收数据报是的存储空间,这里构造器仅使用了两参数(databuffer,datalegth)使用原本开辟的byte数组,后使用receive方法进行接收即可,最后则将获取的数据转为字符串输出以及关闭该socket。
UDPServer.java代码例程:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPServer {
public static void main(String[] args) {
DatagramSocket serverSocket=null;
try {
serverSocket=new DatagramSocket(8991);
while (true){
byte[] recvSentence=new byte[1024];
byte[] sendSentence=new byte[1024];
DatagramPacket receivePacket=new DatagramPacket(recvSentence,0,recvSentence.length);
serverSocket.receive(receivePacket);
String data=new String(receivePacket.getData(),0,receivePacket.getLength());
System.out.println("Receive data:"+data);
InetAddress IPadd=receivePacket.getAddress();
int port=receivePacket.getPort();
data+=" ACK";
sendSentence=data.getBytes();
DatagramPacket sendPacket=new DatagramPacket(sendSentence,sendSentence.length,IPadd,port);
serverSocket.send(sendPacket);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(serverSocket!=null)
{
serverSocket.close();
}
}
}
}
DatagramSocket serverSocket=new DatagramSocket(8991);
首先客户端发送数据指明该端口故需一致(注意到在client端并未直接初始化端口,因udp协议特点仅需将端口号的信息封装进数据报之中即可)。
byte[] recvSentence=new byte[1024];
byte[] sendSentence=new byte[1024];
DatagramPacket receivePacket=new DatagramPacket(recvSentence,0,recvSentence.length);
serverSocket.receive(receivePacket);
String data=new String(receivePacket.getData(),0,receivePacket.getLength());
System.out.println("Receive data:"+data);
这里的接收与客户端的接收例程类似,都是使用数据报进行封装发送/接收。
InetAddress IPadd=receivePacket.getAddress();
int port=receivePacket.getPort();
data+=" ACK";
sendSentence=data.getBytes();
DatagramPacket sendPacket=new DatagramPacket(sendSentence,sendSentence.length,IPadd,port);
serverSocket.send(sendPacket);
发送时可直接通过receivePacket下的getAddress()与getPort()方法获取客户端的进程端口与主机地址,前者返回的类型为InetAddress后者则为int型 ,再往后将接收的数据拼接一下指定内容再转为字节型数组,最后依旧使用DatagramPacket进行封装(这里参数可有偏移量也可无需)及DatagramSocket的对象下的send方法将数据报打出。
总结:
通篇而言无论是udp还是tcp协议的例程实现发送与接收方的代码逻辑及处理流程都类似,都需确定读\写对应。在udp方式中主要使用了DatagramSocket与DatagramPacket进行数据交互(只有一方发送时需指明对方socket进程端口号及主机地址,而接收只需提取建立一DatagramPacket对象进行数据的存储)。而在tcp方式中通篇都使用到了流进行交互(因需建立连接,而该socket-stream-pipeline则为进行可靠通信的管道,一些差错校验及差错恢复都由双方建立的该管道进行,故一旦建立连接则由双方进行维护该连接),若需多个服务端与客户端不同方向的读/写则可使用多线程进行实现。这里只是进行了简单的连接建立,可由此做其他数据交互。
注:tcp实现时使用到的流可以为其他,可按需求而定(因该socket获取的流为所有该输入/输出流的父类)