c_socket通信经典

概念区别不象OSI模型那样明晰,但很实用。TCP/IP模型分为四层,对应于OSI七层模型如下图所示:图6-1 TCP/IP参考模型与OSI模型的近似对应关系在TCP/IP模型中,互联网层是基于无连接互联网络层的分组交换网络。在这一层中主机可以把报文(Packet)发往任何网络,报文独立地传向目标。互联网层定义了报文的格式和协议,这就是IP协议族(Internet Protocol)。互联网层的功能是将报文发送到目的地,主要的设计问题是报文路由和避免阻塞。互联网层上面是传输层,该层的主要功能和OSI模型的该层一样,主要使源和目的主机之间可以进行会话。该层定义了两个端到端的协议,一个是面向连接的传输控制协议TCP,另一个是无连接的用户数据报协议UDP。TCP/IP协议模型中没有会话层和表示层。传输层之上是应用层,它包含所有的高层协议,如远程虚拟终端协议TELNET、文件传输协议FTP、简单邮件传输协议SMTP等。这些高层协议中常见的如TELNET协议,用来允许用户远程登录到另一台UNIX机器;FTP协议用来传输文件,常见的有WU-FTP(Washington University的FTP服务器端程序,是一个免费程序);SMTP协议用来传送email,常见的服务器端程序有netscape等公司制作的程序,也有免费使用的sendmail程序;还有域名系统服务DNS协议,新闻组传送协议NNTP,用于WWW的超文本传输协议HTTP等。主机到网络这一层,在TCP/IP模型中没有详细定义,这里不作介绍。2、 Socket一般描述 由于越来越多的计算机厂商,特别是工作站制造商如Sun等公司采用了Berkeley UNIX,socket接口被广泛采用,以至于现在,socket接口被广泛认可并成为了事实上的工业标准。目前的SYSV、BSD、OSF都将socket接口作为系统的一部分。当时设计如何支持TCP/IP协议时,有两种加入函数的方法,一种是直接加入支持TCP/IP协议的调用,另一种是加入支持一般网络协议的函数,而用参数来指定支持TCP/IP协议。Berkeley采用了后者,这样可以支持多协议族,TCP/IP是协议族之一(PF_INET)。2.1 socket 描述符 前面已经提到过,在UNIX中,进程要对文件进行操作,一般使用open调用打开一个文件进行访问,每个进程都有一个文件描述符表,该表中存放打开的文件描述符。用户使用open等调用得到的文件描述符其实是文件描述符在该表中的索引号,该表项的内容是一个指向文件表的指针。应用程序只要使用该描述符就可以对指定文件进行操作。同样,socket接口增加了网络通信操作的抽象定义,与文件操作一样,每个打开的socket都对应一个整数,我们称它为socket描述符,该整数也是socket描述符在文件描述符表中的索引值。但socket描述符在描述符表中的表项并不指向文件表,而是指向一个与该socket有关的数据结构。BSD UNIX中新增加了一个socket调用,应用程序可以调用它来新建一个socket描述符,注意进程用open只能产生文件描述符,而不能产生socket描述符。socket调用只能完成建立通信的部分工作,一旦建立了一个socket,应用程序可以使用其他特定的调用来为它添加其他详细信息,以完成建立通信的过程。 2.2 从概念上理解socket的使用网络编程中最常见的是客户/服务器模式。以该模式编程时,服务端有一个进程(或多个进程)在指定的端口等待客户来连接,服务程序等待客户的连接信息,一旦连接上之后,就可以按设计的数据交换方法和格式进行数据传输。客户端在需要的时刻发出向服务端的连接请求。这里为了便于理解,提到了这些调用及其大致的功能。使用socket调用后,仅产生了一个可以使用的socket描述符,这时还不能进行通信,还要使用其他的调用,以使得socket所指的结构中使用的信息被填写完。在使用TCP协议时,一般服务端进程先使用socket调
用得到一个描述符,然后使用bind调用将一个名字与socket描述符连接起来,对于Internet域就是将Internet地址联编到socket。之后,服务端使用listen调用指出等待服务请求队列的长度。然后就可以使用accept调用等待客户端发起连接(一般是阻塞等待连接,后面章节会讲到非阻塞的方式),一旦有客户端发出连接,accept返回客户的地址信息,并返回一个新的socket描述符,该描述符与原先的socket有相同的特性,这时服务端就可以使用这个新的socket进行读写操作了。一般服务端可能在accept返回后创建一个新的进程进行与客户的通信,父进程则再到accept调用处等待另一个连接。客户端进程一般先使用socket调用得到一个socket描述符,然后使用connect向指定的服务器上的指定端口发起连接,一旦连接成功返回,就说明已经建立了与服务器的连接,这时就可以通过socket描述符进行读写操作了。下面是在客户和服务端使用TCP时,客户进程和服务进程使用系统调用的该程。使用TCP的客户和服务端使用系统调用的图示使用无连接的UDP协议时,服务端进程创建一个socket,之后调用recvfrom接收客户端的数据报,然后调用sendto将要返回客户端的消息发送给客户进程。客户端也要先创建一个socket,再使用sendto向服务端进程发出请求,使用recvfrom得到返回的消息。(完)计算机基础教程网
前一篇《Visual C#.Net网络程序开发之Socket》中说到:支持Http、Tcp和Udp的类组成了TCP/IP三层模型(请求响应层、应用协议层、传输层)的中间层-应用协议层,该层的类比位于最底层的Socket类提供了更高层次的抽象,它们封装 TCP 和 UDP 套接字的创建,不需要处理连接的细节, 这使得我们在编写套接字级别的协议时,可以更多地尝试使用 TCPClient 、 UDPClient和TcpListener,而不是直接向 Socket 中写。它们之间的这种层次关系示意如下: 可见,TcpClient 类基于 Socket 类构建,这是它能够以更高的抽象程度提供 TCP 服务的基础。正因为这样,许多应用层上的通讯协议,比如FTP(File Transfers Protocol)文件传输协议、HTTP(Hypertext Transfers Protocol)超文本传输协议等都直接创建在TcpClient等类之上。 TCPClient 类使用 TCP 从 Internet 资源请求数据。TCP 协议建立与远程终结点的连接,然后使用此连接发送和接收数据包。TCP 负责确保将数据包发送到终结点并在数据包到达时以正确的顺序对其进行组合。 从名字上就可以看出,TcpClient类专为客户端设计,它为 TCP 网络服务提供客户端连接。TcpClient 提供了通过网络连接、发送和接收数据的简单方法。 若要建立 TCP 连接,必须知道承载所需服务的网络设备的地址(IPAddress)以及该服务用于通讯的 TCP 端口 (Port)。Internet 分配号码机构 (Internet Assigned Numbers Authority, IANA) 定义公共服务的端口号(你可以访问 http://www.iana.org/assignments/port-numbers获得这方面更详细的资料)。IANA 列表中所没有的服务可使用 1,024 到 65,535 这一范围中的端口号。要创建这种连接,你可以
选用TcpClient类的三种构造函数之一: 1、public TcpClient()当使用这种不带任何参数的构造函数时,将使用本机默认的ip地址并将使用默认的通信端口号0。这样情况下,如果本机不止一个ip地址,将无法选择使用。以下语句示例了如何使用默认构造函数来创建新的 TcpClient: TcpClient tcpClientC = new TcpClient(); 2、public TcpClient(IPEndPoint)使用本机IPEndPoint创建TcpClient的实例对象。上一篇介绍过了,IPEndPoint将网络端点表示为IP地址和端口号,在这里它用于指定在建立远程主机连接时所使用的本地网络接口(IP 地址)和端口号,这个构造方法为使用本机IPAddress和Port提供了选择余地。下面的语句示例了如何使用本地终结点创建 TcpClient 类的实例: IPHostEntry ipInfo=Dns.GetHostByName("www.tuha.net");//主机信息 IPAddressList[] ipList=ipInfo.AddressList;//IP地址数组 IPAddress ip=ipList[0];//多IP地址时一般用第一个 IPEndPoint ipEP=new IPEndPoint(ip,4088);//得到网络终结点 try{ TcpClient tcpClientA = new TcpClient(ipLocalEndPoint); } catch (Exception e ) { Console.WriteLine(e.ToString()); }
到这里,你可能会感到困惑,客户端要和服务端创建连接,所指定的IP地址及通信端口号应该是远程服务器的呀!事实上的确如此,使用以上两种构造函数,你所实现的只是
TcpClient实例对象与IP地址和Port端口的绑定,要完成连接,你还需要显式指定与远程主机的连接,这可以通过TcpClient类的Connect方法来实现, Connet方法使用指定的主机名和端口号将客户端连接到 远程主机: 1)、public void Connect(IPEndPoint); 使用指定的远程网络终结点将客户端连接到远程 TCP 主机。 public void Connect(IPAddress, int); 使用指定的 IP 地址和端口号将客户端连接到 TCP 主机。 public void Connect(string, int); 将客户端连接到指定主机上的指定端口。 需要指出的是,Connect方法的所有重载形式中的参数IPEndPoint网络终结点、IPAddress以及表现为string的Dns主机名和int指出的Port端口均指的是远程服务器。 以下示例语句使用主机默认IP和Port端口号0与远程主机建立连接: TcpClient tcpClient = new TcpClient();//创建TcpClient对象实例 try{ tcpClient.Connect("www.contoso.com",11002);//建立连接 } catch (Exception e ){ Console.WriteLine(e.ToString()); }
C#是微软随着VS.net新推出的一门语言。它作为一门新兴的语言,有着C++的强健,又有着VB等的RAD特性。而且,微软推出C#主要的目的是为了对抗Sun公司的Java。大家都知道Java语言的强大功能,尤其在网络编程方面。于是,C#在网络编程方面也自然不甘落后于人。本文就向大家介绍一下C#下实现套接字(Sockets)编程的一些基本知识,以期能使大家对此有个大致了解。首先,我向大家介绍一下套接字的概念。
套接字基本概念:
套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套接字有两种不同的类型:流套接字和数据报套接字。
套接字工作原理:
要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
所谓服务器监听,是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
C#中的套接字编程实例:
通过向大家简单的介绍套接字的基本概念和实现套接字编程的基本原理,我想大家对套接字编程已有了初步的了解。不过,上面介绍的仅仅是基本概念和原理,要真正运用还是需要一定的工作的。对基本概念和原理的真正理解的最好方法莫过于自己动手做一个实例,
下面我就向大家介绍一个很好的用C#实现套接字编程的实例――聊天室程序。
本程序是基于C/S(服务器/客户端)构架的,程序包含一个服务器端的应用程序和一个客户端的应用程序。首先,在服务器上运行服务器端的应用程序,该程序一运行就开始服务器监听。然后,在客户机上就可以打开客户端的应用程序。程序打开后可以与服务器端应用程序进行连接,即进行客户端请求。在连接确认后,客户端用户可以和其他的客户端用户进行聊天。客户端人数没有限制,同时还支持“悄悄话”聊天模式,支持聊天记录。所以这是一个学习套接字编程的相当不错的例子。而且,程序中为了处理每个客户端的信息还用到了多线程机制。在每个客户端与服务器端连接成功后,它们之间就建立一个线程。这样运用了多线程之后,客户端之间就不会相互影响,即使其中一个出了错误也不会影响到另一个。
下面,我就向大家具体介绍该实例:
服务器端程序:
1. 打开VS.net,新建一个C#的模板为“Windows 应用程序”的项目,不妨命名为“ChatServer”。
2. 布置界面。只需在界面上添加一个ListBox控件即可,该控件主要用于显示客户端的用户的一些信息的。图象如下:
3. 服务器端程序的代码编写。
对于服务器端,主要的作用是监听客户端的连接请求并确认其请求。程序一开始便打开一个StartListening()线程。
private void StartListening()
{
listener = new TcpListener(listenport);
listener.Start();
while (true)
{
try
{
Socket s = listener.AcceptSocket();
clientsocket = s;
clientservice = new Thread(new ThreadStart(ServiceClient));
clientservice.Start();
}
catch(Exception e)
{
Console.WriteLine(e.ToString() );
}
}
}
该线程是一直处于运行状态的。当服务器端接收到一个来自客户端的连接请求后,它就打开一个ServiceClient()线程来服务客户端。当一个连接被建立后,每个客户端就被赋予一个属于它自己的套接字。同时,一个Client类的对象被建立。该对象包含了客户端的一些相关信息,该信息被保存在一个数组列表中。 Client类如下:
using System;
using System.Threading;
namespace ChatServer
{
using System.Net.Sockets;
using System.Net;
///
/// Client 的摘要说明。
///
public class Client
{
private Thread clthread;
private EndPoint endpoint;
private string name;
private Socket sock;
public Client(string _name, EndPoint _endpoint, Thread _thread, Socket _sock)
{
// TODO: 在此处添加构造函数逻辑
clthread = _thread;
endpoint = _endpoint;
name = _name;
sock = _sock;
}
public override string ToString()
{
return endpoint.ToString()+ " : " + name;
}
public Thread CLThread
{
get{return clthread;}
set{clthread = value;}
}
public EndPoint Host
{
get{return endpoint;}
set{endpoint = value;}
}
public string Name
{
get{return name;}
set{name = value;}
}
public Socket Sock
{
get{return sock;}
set{sock = value;}
}
}
}
程序的主体部分应是ServiceClient()函数。该函数是一个独立的线程,其主要部分是一个while循环。在循环体内,程序处理各种客户端命令。服务器端接收来自客户端的以ASCII码给出的字符串,其中包含了一个“|”形式的分隔符。字符串中“|”以前的部分就是具体的命令,包括CONN、CHAT、PRIV、GONE四种类型。CONN命令建立一个新的客户端连接,将现有的用户列表发送给新用户并告知其他用户有一个新用户加入。CHAT命令将新的信息发送给所有用户。PRIV命令将悄悄话发送给某个用户。GONE命令从用户列表中除去一个已离开的用户并告知其他的用户某某已经离开了。同时,GONE命令可以设置布尔型的变量keepalive为false从而结束与客户端连接的线程。ServiceClient()函数如下:
private void ServiceClient()
{
Socket client = clientsocket;
bool keepalive = true;
while (keepalive)
{
Byte[] buffer = new Byte[1024];
client.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
{
Client cl = (Client)clients[n];
SendToClient(cl, "JOIN|" + tokens[1]);
}
EndPoint ep = client.RemoteEndPoint;
Client c = new Client(tokens[1], ep, clientservice, client);
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
{
Client cl = (Client)clients[n];
SendToClient(cl, clientcommand);
}
}
if (tokens[0] == "PRIV")
{
string destclient = tokens[3];
for(int n=0; 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
{
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);
client.Close();
keepalive = false;
}
}
}
这样,服务器端程序就基本完成了。(其他略次要的代码可以参见源代码中的Form1.cs文件)程序运行图示如下:
客户端程序:
1. 打开VS.net,新建一个C#的模板为“Windows 应用程序”的项目,不妨命名为“ChatClient”。
2. 布置界面。往界面上添加一个ListBox控件(用于显示用户列表),一个RichTextBox控件(用于显示聊天消息以及系统消息),一个TextBox控件(用于发送消息),一个CheckBox控件(确定是否为悄悄话),一个StatusBar控件以及四个Button控件(分别为“连接”、“断开连接”、“开始记录”、“发送”)。各个控件的属性设置可以参见源代码中的具体设置,这里从略。界面设计好后的图象如下:
3. 客户端程序的代码编写。
当客户端试图和服务器端进行连接时,一个连接必须建立而且得向服务器端进行注册。 EstablishConnection()函数运用一个TcpClient来和服务器端取得连接,同时创建一个NetworkStream来发送消息。还有,端口号和服务器端的是保持一致的,均为5555。EstablishConnection()函数如下:
private void EstablishConnection()
{
statusBar1.Text = "正在连接到服务器";
try
{
clientsocket = new TcpClient(serveraddress,serverport);
ns = clientsocket.GetStream();
sr = new StreamReader(ns);
connected = true;
}
catch (Exception)
{
MessageBox.Show("不能连接到服务器!","错误",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
statusBar1.Text = "已断开连接";
}
}
在和服务器端连接成功后,程序就用RegisterWithServer()函数向服务器端发送一个CONN命令。该命令先是发送该用户的名称,然后从服务器端获得其他所有用户的列表,所有用户列表是在ListBox控件中显示的。该函数如下:
private void RegisterWithServer()
{
try
{
string command = "CONN|" + ChatOut.Text;
Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray());
ns.Write(outbytes,0,outbytes.Length);
string serverresponse = sr.ReadLine();
serverresponse.Trim();
string[] tokens = serverresponse.Split(new Char[]{'|'});
if(tokens[0] == "LIST")
{
statusBar1.Text = "已连接";
btnDisconnect.Enabled = true;
}
for(int n=1; n
lbChatters.Items.Add(tokens[n].Trim(new char[]{'\r','\n'}));
this.Text = clientname + ":已连接到服务器";
}
catch (Exception)
{
MessageBox.Show("注册时发生错误!","错误",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
在此之后,当然就是用户之间的聊天了,由ReceiveChat()函数来完成。该函数是一个独立的线程,它处理所有用户获得的消息和用户发送的消息。它主要处理了CHAT、PRIV、JOIN、GONE、QU99v等命令,处理的方法和服务器端的类似。具体函数实现如下:
private void ReceiveChat()
{
bool keepalive = true;
while (keepalive)
{
try
{
Byte[] buffer = new Byte[2048];
ns.Read(buffer,0,buffer.Length);
string chatter = System.Text.Encoding.ASCII.GetString(buffer);
string[] tokens = chatter.Split(new Char[]{'|'});
if (tokens[0] == "CHAT")
{
rtbChatIn.AppendText(tokens[1]);
if(logging)
logwriter.WriteLine(tokens[1]);
}
if (tokens[0] == "PRIV")
{
rtbChatIn.AppendText("Private from ");
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(tokens[2] + "\r\n");
if(logging)
{
logwriter.Write("Private from ");
logwriter.Write(tokens[1].Trim() );
logwriter.WriteLine(tokens[2] + "\r\n");
}
}
if (tokens[0] == "JOIN")
{
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(" has joined the Chat\r\n");
if(logging)
{
logwriter.WriteLine(tokens[1]+" has joined the Chat");
}
string newguy = tokens[1].Trim(new char[]{'\r','\n'});
lbChatters.Items.Add(newguy);
}
if (tokens[0] == "GONE")
{
rtbChatIn.AppendText(tokens[1].Trim() );
rtbChatIn.AppendText(" has left the Chat\r\n");
if(logging)
{
logwriter.WriteLine(tokens[1]+" has left the Chat");
}
lbChatters.Items.Remove(tokens[1].Trim(new char[]{'\r','\n'}));
}
if (tokens[0] == "QU99v")
{
ns.Close();
clientsocket.Close();
keepalive = false;
statusBar1.Text = "服务器端已停止";
connected= false;
btnSend.Enabled = false;
btnDisconnect.Enabled = false;
}
}
catch(Exception){}
}
}
通过以上的一些函数,客户端程序之间就可以进行自由地聊天了,各个用户之间还可以互相发送悄悄话。所以程序已经实现了聊天室的基本功能了,不过最后各个用户还要正常地退出,那就要用到QuitChat()函数了。该函数的具体实现如下:
private void QuitChat()
{
if(connected)
{
try
{
string command = "GONE|" + clientname;
Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray());
ns.Write(outbytes,0,outbytes.Length);
clientsocket.Close();
}
catch(Exception)
{
}
}
if(logging)
logwriter.Close();
if(receive != null && receive.IsAlive)
receive.Abort();
this.Text = "客户端";
}
到此为止,客户端程序的主要部分都已经介绍完毕。还有一些按钮控件的消息处理函数可以参见源代码。同时,程序中还有一个聊天记录功能,该功能和现在流行的聊天软件的记录功能类似。不过限于篇幅,在这里就不一一介绍了,有兴趣的读者可以研究一下本文后面的源代码。
这样,客户端程序就完成了。程序运行图示如下:
总结:
本文向大家初步介绍了套接字的基本概念和实现套接字编程的基本原理,还通过一个很好的实例向大家展示了在C#下进行套接字编程的实现方法和一些编程技巧。从中,我们不难发现运用C#进行套接字编程乃至网络编程有许多优越之处。实例程序实现的思路清晰明了而且通俗易懂,是一个相当不错的例子,希望各位能好好研读。同时还希望大家能进一步完善该程序,使之功能更强大、界面更友好
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值