TCP、IP或UDP这3种协议实现数据传输。
在传输数据的过程中,
需要通过一个双向的通信连接实现数据的交互。
在这个传输过程中,
通常将这个双向链路的一端称为Socket,
一个Socket通常由一个IP地址和一个端口号来确定。
在整个数据传输过程中,Socket的作用是巨大的。
在Java编程应用中,Socket是Java网络编程的核心。网络编程中有两个主要的问题,
一个是如何准确地定位网络上一台或多台主机,
另一个就是找到主机后如何可靠高效地进行数据传输。
在TCP/IP协议中
IP层主要负责网络主机的定位,数据传输的路由,
由IP地址可以唯一地确定Internet上的一台主机。
TCP层则
提供面向应用的可靠(TCP)的
或非可靠(UDP)的数据传输机制,
这是网络编程的主要对象,
一般不需要关心IP 层是如何处理数据的。
目前较为流行的网络编程模型是客户机/服务器(C/S)结构。
即通信双方,一方作为服务器
等待(另一方作为的)客户提出请求并予以响应。
客户则在需要服务时向服务器提出申请。
服务器一般作为守护进程 始终运行,
监听网络端口,
一旦有客户请求,就会启动一个服务进程来响应该客户,
同时自己继续监听服务端口,
使后来的客户也能及时得到服务。TCP/IP是Transmission Control Protocol/Internet Protocol的简写,
中译名为传输控制协议/因特网协议,
又名网络通信协议,
是Internet最基本的协议、Internet国际互联网络的基础,
由网络层的IP协议和
传输层的TCP协议组成。
TCP/IP定义了电子设备如何连入因特网,
以及数据如何在它们之间传输的标准。
TCP/IP协议采用了4层的层级结构,
每一层都呼叫它的下一层所提供的协议来完成自己的需求。
也就是说,
TCP负责发现传输的问题,
一旦发现问题便发出信号要求重新传输,
直到所有数据安全正确地传输到目的地。
而IP的功能是给因特网的每一台电脑规定一个地址。
TCP/IP协议不是TCP和IP这两个协议的合称,
而是指因特网整个TCP/IP协议簇。
从协议分层模型方面来讲,TCP/IP由4个层次组成,
分别是网络接口层、网络层、传输层、应用层。
TCP/IP协议并不完全符合OSI(Open System Interconnect)的7层参考模型,
OSI是传统的开放式系统互连参考模型,
是一种通信协议的7层抽象的参考模型,
其中每一层执行某一特定任务。
该模型的目的是
使各种硬件在相同的层次上相互通信。
这7层是
物理层、数据链路层(网络接口层)、
网络层(网络层)、
传送层(传输层)、
会话层、表示层和应用层(应用层)。
而TCP/IP协议采用了4层的层级结构,
每一层都呼叫它的下一层所提供的网络来完成自己的需求。
由于ARPANET的设计者注重的是网络互联,
允许通信子网(网络接口层)采用已有的或是将来有的各种协议,
所以这个层次中没有提供专门的协议。
实际上,
TCP/IP协议可以通过网络接口层连接到任何网络上,
例如X.25交换网或IEEE802局域网。UDP是User Datagram Protocol的简称,
是一种无连接的协议,
每个数据报都是一个独立的信息,
包括完整的源地址或目的地址,
它在网络上以任何可能的路径传往目的地,
因此能否到达目的地,
到达目的地的时间以及内容的正确性都是不能被保证的。
在现实网络数据传输过程中,
大多数功能是由TCP协议和UDP协议实现。面向连接的协议,
在Socket之间进行数据传输之前必然要建立连接,
所以在TCP中需要连接时间。
TCP传输数据大小限制,
一旦连接建立起来,
双方的Socket就可以按统一的格式传输大的数据。
TCP是一个可靠的协议,
它确保接收方完全正确地获取发送方所发送的全部数据。数据报中都给出了完整的地址信息,
因此无需要建立发送方和接收方的连接。
UDP传输数据时是有大小限制的,
每个被传输的数据报必须限定在64KB之内。
UDP是一个不可靠的协议,
发送方所发送的数据报并不一定以相同的次序到达接收方。网络通信上有极强的生命力,
例如远程连接(Telnet)和文件传输(FTP)
都需要不定长度的数据被可靠地传输。
但是可靠的传输是要付出代价的,
对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,
因此TCP传输的效率不如UDP高。操作简单,而且仅需要较少的监护,
因此通常用于局域网高可靠性的分散系统中Client/Server 应用程序。
例如视频会议系统,
并不要求音频视频数据 绝对的正确,
只要保证连贯性就可以了,
这种情况下显然使用UDP会更合理一些,
因为TCP和UDP都能达到这个保证连贯性的门槛,
但是TCP却要多占用更多的计算机资源,
杀鸡焉用牛刀呢,
所有这种情况不用TCP,用UDP。双向的通信连接实现数据的交换,
这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。
Socket是TCP/IP协议的一个十分流行的编程方式,
一个Socket由一个IP地址和一个端口号 唯一确定。
但是,
Socket所支持的协议种类也不光TCP/IP一种,
因此两者之间是没有必然联系的。
在Java环境下,
Socket编程主要是指基于TCP/IP协议的网络编程。Server端Listen(监听)某个端口是否有连接请求,
Client端向Server 端发出Connect(连接)请求,
Server端向Client端发回Accept(接收)消息,
一个连接就建立起来了。Server端和Client端都可以通过Send、Write等方法与对方通信。Java网络编程应用中,
对于一个功能齐全的Socket来说,
其工作过程包含如下所示的基本步骤。
(1)创建ServerSocket和Socket;
(2)打开连接到Socket的输入/输出流;
(3)按照一定的协议对Socket进行读/写操作;
(4)关闭IO流和Socket。Java网络编程应用中,
包java.net中提供了两个类Socket和ServerSocket,
分别用来表示双向连接的客户端和服务端。
这是两个封装得非常好的类,
其中包含了如下所示的构造方法:Socket(InetAddress address, int port);Socket(InetAddress address, int port, boolean stream);Socket(String host, int prot);Socket(String host, int prot, boolean stream);Socket(SocketImpl impl);Socket(String host, int port, InetAddress localAddr, int localPort);Socket(InetAddress address, int port, InetAddress localAddr, int localPort);ServerSocket(int port);ServerSocket(int port, int backlog);ServerSocket(int port, int backlog, InetAddress bindAddr)构造方法中,
参数address、host和port分别是
双向连接中另一方的IP地址、主机名和端口号,
stream指明Socket是流Socket还是数据报Socket,
localPort表示本地主机的端口号,
localAddr和bindAddr是本地机器的地址(ServerSocket的主机地址),
impl是Socket的父类,
既可以用来创建ServerSocket又可以用来创建Socket。
例如: Socket client = new Socket("127.0.0.1", 80);
ServerSocket server = new ServerSocket(80);端口,
每一个端口提供一种特定的服务,
只有给出正确的端口,才能获得相应的服务。
0~1023的端口号为系统所保留,
例如
HTTP服务的端口号为80,
Telnet服务的端口号为21,
FTP服务的端口号为23,
所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。
另外,
在创建Socket时如果发生错误,将产生IOException,
在程序中必须对之做出处理。
所以在创建Socket或ServerSocket时必须捕获或抛出异常。TCP/IP通信协议是一种可靠的网络协议,
能够在通信的两端各建立一个Socket,
从而在通信的两端之间形成网络虚拟链路。
一旦建立了虚拟的网络链路,
两端的程序就可以通过虚拟链路进行通信。
Java语言对TCP网络通信提供了良好的封装,
通过Socket对象代表两端的通信端口,
并通过Socket产生的IO流进行网络通信。
这里先笔记Java应用中TCP编程的基本知识,
为后面的Android编程打下基础。在Java程序中,
类ServerSocket 接受其他通信实体的连接请求。
对象ServerSocket的功能是监听来自客户端的Socket连接,
如果没有连接则会一直处于等待状态。ServerSocket中包含了如下监听客户端连接请求的方法:
Socket accept():如果接收到一个客户端Socket的连接请求,
该方法将返回一个与客户端Socket对应的Socket,
否则该方法将一直处于等待状态,线程也被阻塞。ServerSocket对象,
ServerSocket类为我们提供了如下构造器:
ServerSocket(int port):
用指定的端口port创建一个ServerSocket,
该端口应该是有一个有效的端口整数值0~65535。ServerSocket(int port,int backlog):
增加一个用来改变连接队列长度的参数backlog。ServerSocket(int port,int backlog,InetAddress localAddr):
在机器(服务器、本机等)存在多个IP地址的情况下,
允许通过localAddr这个参数
来指定将ServerSocket绑定到指定的IP地址。ServerSocket后,
需要使用ServerSocket中的方法close()关闭该ServerSocket。不会只接受一个客户端请求,
而是会不断地接受来自客户端的所有请求,
所以可以通过循环来不断地调用ServerSocket中的方法accept()。 //创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss = new ServerSocket(30000);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = ss.accept();
//下面就可以使用Socket进行通信了
...
}ServerSocket没有指定IP地址,
该ServerSocket会绑定到本机默认的IP地址。
在代码中使用30000作为该ServerSocket的端口号,
通常推荐使用10000以上的端口,
主要是为了避免与其他应用程序的通用端口 冲突。Socket的构造器
实现``和指定服务器的连接,
在Socket中可以使用如下两个构造器:
Socket(InetAddress/String remoteAddress, int port):
创建连接到指定远程主机、远程端口的Socket,
该构造器没有指定本地地址、本地端口,
本地IP地址和端口使用默认值。Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort):
创建连接到指定远程主机、远程端口的Socket,
并指定本地IP地址和本地端口号,
适用于本地主机有多个IP地址的情形。构造器指定远程主机时,
既可使用InetAddress来指定,也可以使用String对象指定,
在Java中通常使用String对象指定远程IP,例如192.168.2.23。
当本地主机只有一个IP地址时,建议使用第一个方法,简单方便。
例如下面的代码: //创建连接到本机、30000端口的Socket
Socket s = new Socket("127.0.0.1" , 30000);上述代码后会连接到指定服务器,
让服务器端的ServerSocket的方法accept()向下执行,
于是服务器端和客户端就产生一对互相连接的Socket。
上述代码连接到“远程主机”的IP地址是127.0.0.1,
此IP地址总是代表本机的IP地址。
这里例程的服务器端、客户端都是在本机运行,
所以Socket连接到远程主机的IP地址使用127.0.0.1。客户端、服务器端产生对应的Socket之后,
程序无须再区分服务器端和客户端,
而是通过各自的Socket进行通信。Socket中提供如下两个方法获取输入流和输出流:
InputStream getInputStream():
返回该Socket对象 对应的输入流,
让程序通过该输入流从Socket中取出数据。OutputStream getOutputStream():
返回该 Socket对象 对应的输出流,
让程序通过该输出流 向Socket中输出数据。public class Server
{
public static void main(String[] args)
throws IOException
{
//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket myss = new ServerSocket(30001);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = myss.accept();
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
//进行普通IO操作
ps.println("凌川江雪!");
ps.println("望川霄云!");
ps.println("万年太久,只争朝夕!");
ps.println("人间正道是沧桑!");
ps.println("穷善其身,达济天下!");
//关闭输出流,关闭Socket
ps.close();
s.close();
}
}
}ServerSocket监听,
并且使用Socket获取了输出流,
执行后不会显示任何信息。public class Client {
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1" , 30001);
//将Socket对应的输入流包装成BufferedReader
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
//进行普通IO操作
StringBuilder response = new StringBuilder();
String line;
//一行一行地读取并加进stringbuilder
while((line = br.readLine()) != null){
response.append(line + "\n");
}
System.out.println("来自服务器的数据:" + "\n" + response.toString());
//关闭输入流、Socket
br.close();
socket.close();
}
}先运行服务端Class,再运行客户端Class,运行结果:

ServerSocket和Socket建立网络连接之后,
程序通过网络通信与普通IO并没有太大的区别。
如果先运行上面程序中的Server 类,
将看到服务器一直处于等待状态,
因为服务器使用了死循环来接受来自客户端的请求;
再运行Client类,
将可看到程序输出“来自服务器的数据:...!”,
这表明客户端和服务器端通信成功。Server和Client只是进行了简单的通信操作,
当服务器接收到客户端连接之后,服务器向客户端输出一个字符串,
而客户端也只是读取服务器的字符串后就退出了。客户端可能需要和服务器端保持长时间通信,
即服务器需要不断地读取客户端数据,
并向客户端写入数据,
客户端也需要不断地读取服务器数据,
并向服务器写入数据。readLine()方法读取数据时,
如果在该方法成功返回之前线程被阻塞,则程序无法继续执行。
所以服务器很有必要为每个Socket 单独启动一条线程,
每条线程 负责与一个客户端进行通信。客户端读取服务器数据的线程同样会被阻塞,
所以系统应该单独 启动一条线程,
该组线程专门负责读取服务器数据。聊天室程序,
在服务器端应该包含多条线程,
其中每个Socket对应一条线程,
该线程负责
读取 Socket 对应输入流的数据
(从客户端发送过来的数据),
并将读到的数据
向每个Socket输出流发送一遍
(将一个客户端 发送的数据 “广播” 给 其他客户端 );服务器端使用List来保存所有的Socket。
在具体实现时,
为服务器提供了如下两个类:
创建ServerSocket监听的主类。
处理每个Socket通信的线程类。1/4 接下来介绍具体实现流程,首先看下面的IServerClass:
public class IServer
{
//定义保存所有Socket的ArrayList
public static ArrayList<Socket> socketList = new ArrayList<Socket>();
public static void main(String[] args)
throws IOException
{
ServerSocket ss = new ServerSocket(30000);
while(true)
{
//此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
//每当客户端连接后启动一条ServerThread线程为该客户端服务
new Thread(new Serverxian(s)).start();
}
}
}
IServer类中,服务器端(ServerSocket )只负责接受客户端Socket的连接请求, 每当客户端Socket连接到该ServerSocket之后, 程序将客户端对应的Socket(客户Socket的对面一端)加入socketList集合中保存, 并为该Socket启动一条线程(Serverxian), 该线程负责处理 该Socket所有 的 通信任务。 小结:IServer类完成的业务是: 1.接收客户端Socket, 2.保存对应返回的Socket, 3.启动处理线程。
2/4 接着看服务器端线程类文件:
package liao.server;
import java.io.*;
import java.net.*;
import java.util.*;
//负责处理每个线程通信的线程类
public class Serverxian implements Runnable
{
//定义当前线程所处理的Socket
Socket s = null;
//该线程所处理的Socket所对应的输入流读取器
BufferedReader br = null;
public Serverxian(Socket s)
throws IOException
{
this.s = s;
//初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
public void run()
{
try
{
String content = null;
//采用循环不断从Socket中读取客户端发送过来的数据
while ((content = readFromClient()) != null)
{
//遍历socketList中的每个Socket,
//将读到的内容向每个Socket发送一次
for (Socket s : IServer.socketList)
{
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}
catch (IOException e)
{
//e.printStackTrace();
}
}
//定义读取客户端数据的方法
private String readFromClient()
{
try
{
return br.readLine();
}
//如果捕捉到异常,表明该Socket对应的客户端已经关闭
catch (IOException e)
{
//删除该Socket。
IServer.socketList.remove(s);
}
return null;
}
}Serverxian类(服务器端线程类)中, 注意是线程类,继承Runnable,重写run方法 会不断读取客户端数据,
在获取时使用方法readFromClient()来读取客户端数据。 如果读取数据过程中捕获到 IOException异常, 则说明此Socket对应的客户端Socket出现了问题, 程序就会将此Socket从socketList中删除。
当服务器线程读到客户端数据之后会遍历整个socketList集合, 并将该数据向socketList集合中的每个Socket发送一次, 该服务器线程将把从Socket中读到的数据 向socketList中的每个Socket转发一次。
Socket输入流中的内容,
当获取Socket输入流中的内容后,
直接将这些内容打印在控制台。
先运行上面程序中的类IServer,
该类运行后作为本应用的服务器,不会看到任何输出。接着可以运行多个 IClient——相当于启动多个聊天室客户端登录该服务器,此时在任何一个客户端通过键盘输入一些内容后单击“回车”键,将可看到所有客户端(包括自己)都会在控制台收到刚刚输入的内容,这就简单实现了一个聊天室的功能。
图中展现的是:已经启动服务端, 同时启动两个客户端, 来回切换客户端进行“聊天”, 客户端由于服务端的socket传输, 可以相互收到彼此的信息;