TCP/IP 体系结构与特点
TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输层服务,而IP则是提供网络层服务。TCP/IP包括以下协议:
IP:网间协议(Internet Protocol) 负责主机间数据的路由和网络上数据的存储。同时为ICMP,TCP,UDP提供分组发送服务。用户进程通常不需要涉及这一层。
ARP:地址解析协议(Address Resolution Protocol)
此协议将网络地址映射到硬件地址。
RARP:反向地址解析协议(Reverse Address Resolution Protocol)
此协议将硬件地址映射到网络地址
ICMP:网间报文控制协议(Internet Control Message Protocol)
此协议处理信关和主机的差错和传送控制。
TCP:传送控制协议(Transmission Control Protocol)
这是一种提供给用户进程的可靠的全双工字节流面向连接的协议。它要为用户进程提供虚电路服务,并为数据可靠传输建立检查。(注:大多数网络用户程序使用TCP)
UDP:用户数据报协议(User Datagram Protocol)
这是提供给用户进程的无连接协议,用于传送数据而不执行正确性检查。
FTP:文件传输协议(File Transfer Protocol)
允许用户以文件操作的方式(文件的增、删、改、查、传送等)与另一主机相互通信。
SMTP:简单邮件传送协议(Simple Mail Transfer Protocol)
SMTP协议为系统之间传送电子邮件。
TELNET:终端协议(Telnet Terminal Procotol)
允许用户以虚终端方式访问远程主机
HTTP:超文本传输协议(Hypertext Transfer Procotol)
TFTP: 简单文件传输协议(Trivial File Transfer Protocol)
TCP/IP协议的核心部分是传输层协议(TCP、UDP),网络层协议(IP)和物理接口层,这三层通常是在操作系统内核中实现。因此用户一般不涉及。 编程时,编程界面有两种形式:一、是由内核心直接提供的系统调用;二、使用以库函数方式提供的各种函数。前者为核内实现,后者为核外实现。用户服务要通过 核外的应用程序才能实现,所以要使用套接字(socket)来实现。
套接字基本概念:
套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个 网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数 据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套接字有两种不同的类型:流套接字和数据报套接字。
(1)流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的TCP服务应用;
(2)数据报式套接字(SOCK_DGRAM):一种无连接的Socket,对应于无连接的UDP服务应用。
从用户的角度来看,SOCK_STREAM、SOCK_DGRAM这两类套接字似乎的确涵盖了TCP/IP应用的全部,因为基于TCP/IP的应用,从 协议栈的层次上讲,在传输层的确只可能建立于TCP或UDP协议之上,而SOCK_STREAM、SOCK_DGRAM又分别对应于TCP和UDP,所以 几乎所有的应用都可以用这两类套接字实现。
套接字工作原理:
要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
所谓服务器监听,是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述 发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
典型过程图
面向连接的套接字的系统调用时序图
无连接协议的套接字调用时序图
C#实现套接字编程的实例――聊天室程序
本程序是基于C/S(服务器/客户端)构架的,程序包含一个服务器端的应用程序和一个客户端的应用程序。
首先,在服务器上运行服务器端的应用程序,该程序一运行就开始服务器监听。
然后,在客户机上就可以打开客户端的应用程序。程序打开后可以与服务器端应用程序进行连接,即进行客户端请求。在连接确认后,客户端用户可以和其他的客户端用户进行聊天。客户端人数没有限制,同时还支持“悄悄话”聊天模式,支持聊天记录。
所以这是一个学习套接字编程的相当不错的例子。而且,程序中为了处理每个客户端的信息还用到了多线程机制。在每个客户端与服务器端连接成功后,它们之间就建立一个线程。这样运用了多线程之后,客户端之间就不会相互影响,即使其中一个出了错误也不会影响到另一个。
服务器端程序:
布置界面。只需在界面上添加一个ListBox控件即可,该控件主要用于显示客户端的用户的一些信息的。
服务器端程序的代码编写:
对于服务器端,主要的作用是监听客户端的连接请求并确认其请求。程序一开始便打开一个StartListening()线程。
开始定义如下变量:
private System.Net.Sockets.TcpListener theListener;
private System.Net.Sockets.Socket clientSocket;
private System.Threading.Thread clientService;
StartListening()函数如下:
private void StartListening()
{
theListener = new System.Net.Sockets.TcpListener(listenPort); //listenPort为端口号
theListener.Start();
while (true)
{
try
{
System.Net.Sockets.Socket s = theListener.AcceptSocket();
clientSocket = s;
clientService = new System.Threading.Thread(new System.Threading.ThreadStart(ServiceClient));
clientService.Start();
}
catch(Exception e)
{
Console.WriteLine(e.ToString() );
}
}
}
该线程是一直处于运行状态的。
当服务器端接收到一个来自客户端的连接请求后,它就打开一个ServiceClient()线程来服务客户端。当一个连接被建立后,每个客户端就被赋予一个属于它自己的套接字。同时,一个Client类的对象被建立。该对象包含了客户端的一些相关信息,该信息被保存在一个数组列表中。
Client类如下:
using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
namespace ChatServer
{
public class Client
{
private Thread clientThread;
private EndPoint theEndPoint;
private string name;
private Socket sock;
public Client(string _name, EndPoint _endpoint, Thread _thread, Socket _sock)
{
clientThread = _thread;
theEndpoint = _endpoint;
name = _name;
sock = _sock;
}
public override string ToString()
{
return TheEndpoint.ToString()+ " : " + name;
}
public Thread CLThread
{
get{return clientThread;}
set{clientThread = value;}
}
public EndPoint Host
{
get{return theEndpoint;}
set{theEndpoint = value;}
}
public string TheName
{
get{return name;}
set{name = value;}
}
public Socket TheSock
{
get{return sock;}
set{sock = value;}
}
}
}
程序的主体部分应是ServiceClient()函数。该函数是一个独立的线程,其主要部分是一个while循环。在循环体内,程序处理各种客户端命令。
服务器端接收来自客户端的以ASCII码给出的字符串,其中包含了一个"|"形式的分隔符。
字符串中"|"以前的部分就是具体的命令,包括CONN、CHAT、PRIV、GONE四种类型。
CONN命令建立一个新的客户端连接,将现有的用户列表发送给新用户并告知其他用户有一个新用户加入。
CHAT命令将新的信息发送给所有用户。
PRIV命令将悄悄话发送给某个用户。
GONE命令从用户列表中除去一个已离开的用户并告知其他的用户某某已经离开了。同时,GONE命令可以设置布尔型的变量keepAlive为false从而结束与客户端连接的线程。
ServiceClient()函数如下:
private void ServiceClient()
{
System.Net.Sockets.Socket clientS = clientSocket;
bool keepAlive = true;
while (keepAlive)
{
Byte[] buffer = new Byte[1024];
clientS.Receive(buffer);
string clientCommand = System.Text.Encoding.ASCII.GetString(buffer);
string[] tokens = clientCommand.Split(new Char[]{'|'});
Console.WriteLine(clientCommand);
if (tokens[0] == "CONN")
{
for(int n=0; n<clients.Count;n++)
{
Client cl = (Client)clients[n];
SendToClient(cl, "JOIN|" + tokens[1]);
}
System.Net.EndPoint ep = clientS.RemoteEndPoint;
Client c = new Client(tokens[1], ep, clientService, clientS);
clients.Add(c);
string message = "LIST|" + GetChatterList() +"\r\n";
SendToClient(c, message);
lbClients.Items.Add(c);
}
if (tokens[0] == "CHAT")
{
for(int n=0; n<clients.Count;n++)
{
Client cl = (Client)clients[n];
SendToClient(cl, clientCommand);
}
}
if (tokens[0] == "PRIV")
{
string destClient = tokens[3];
for(int n=0; n<clients.Count;n++)
{
Client cl = (Client)clients[n];
if(cl.Name.CompareTo(tokens[3]) == 0)
SendToClient(cl, clientCommand);
if(cl.Name.CompareTo(tokens[1]) == 0)
SendToClient(cl, clientCommand);
}
}
if (tokens[0] == "GONE")
{
int remove = 0;
bool found = false;
int c = clients.Count;
for(int n=0; n<clients.Count;n++)
{
Client cl = (Client)clients[n];
SendToClient(cl, clientCommand);
if(cl.Name.CompareTo(tokens[1]) == 0)
{
remove = n;
found = true;
lbClients.Items.Remove(cl);
}
}
if(found)
clients.RemoveAt(remove);
clientS.Close();
keepAlive = false;
}
}
这样,服务器端程序就基本完成了。