C#中使用Socket向服务器端发送并接收回应消息的代码
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void StartListening()
{
TcpListener listener = new TcpListener(listenport);
listener.Start();
label1.Text = "listening....";
while (true)
{
try
{
Socket s = listener.AcceptSocket();
clientsocket = s;
clientservice = new Thread(new ThreadStart(ServiceClient));
clientservice.Start();
}
catch(Exception ex)
{
MessageBox.Show("listening Error: "+ex.Message);
}
}
}
private void ServiceClient()
{
Socket client = clientsocket;
bool keepalive = true;
while (keepalive)
{
Byte[] buffer = new Byte[1024];
int bufLen = 0;
try
{
bufLen = client.Available ;
client.Receive(buffer,0,bufLen,SocketFlags.None);
if(bufLen==0)
continue;
}
catch(Exception ex)
{
MessageBox.Show("Receive Error:"+ex.Message);
return;
}
string clientcommand = System.Text.Encoding.ASCII.GetString(buffer).Substring(0,bufLen);
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]);
}
EndPoint ep = client.RemoteEndPoint;
Client c = new Client(tokens[1], ep, clientservice, client);
string message = "LIST|" + GetChatterList() +"/r/n";
SendToClient(c, message);
clients.Add(c);
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);
client.Close();
keepalive = false;
}
}
}
private string GetChatterList()
{
string result = "";
for(int i=0;i<clients.Count;i++)
{
result += ((Client)clients[i]).Name+"|";
}
return result;
}
private void SendToClient(Client cl,string clientCommand)
{
Byte[] message = System.Text.Encoding.ASCII.GetBytes(clientCommand);
Socket s = cl.Sock;
if(s.Connected)
{
s.Send(message,message.Length,0);
}
}
private void Form1_Load(object sender, System.EventArgs e)
{
clients = new ArrayList();
}
private void button1_Click(object sender, System.EventArgs e)
{
threadListen = new Thread(new ThreadStart(StartListening));
threadListen.Start();
}
private void ReceiveChat()
{
bool keepalive = true;
while (keepalive)
{
try
{
Byte[] buffer = new Byte[1024]; // 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] == "QUIT")
{
ns.Close();
clientsocket.Close();
keepalive = false;
statusBar1.Text = "服务器端已停止";
connected= false;
btnSend.Enabled = false;
btnDisconnect.Enabled = false;
}
}
catch(Exception){}
}
}
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 ex)
{
MessageBox.Show(ex.Message);
}
}
// if(logging)
// logwriter.Close();
if(receive != null && receive.IsAlive)
receive.Abort();
this.Text = "客户端";
connected = false;
}
private void btnSend_Click(object sender, System.EventArgs e)
{
if(connected)
{
try
{
string command = "CHAT|" + clientname+": "+ChatOut.Text+"/r/n";
Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray());
ns.Write(outbytes,0,outbytes.Length);
//clientsocket.Close();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
private void btnConnect_Click(object sender, System.EventArgs e)
{
EstablishConnection();
RegisterWithServer();
if(connected)
{
receive = new Thread(new ThreadStart(ReceiveChat));
receive.Start();
}
}
private void btnDisconnect_Click(object sender, System.EventArgs e)
{
QuitChat();
}
五个I/O模型
阻塞I/O
非阻塞I/O
I/O复用(select和poll)
信号驱动I/O(SIGIO)
异步I/O
阻塞 I/O模型
进程调用recvfrom,此系统调用直到数据报到达且拷贝到应用缓冲区或是出错才返回。最常见的错误是系统调用被信号中断,进程阻塞的整段时间是指从调用recvfrom开始到它返回的这段时间,当进程返回成功指示时,应用进程开始处理数据报。
非阻塞方式
当请求的I/O操作不能完成时,不让进程睡眠,而应返回一个错误。 前三次调用recvfrom时仍无数据返回,因此内核立即返回一个错误。第四次调用recvfrom时,数据报已准备好,被拷贝到应用缓冲区, recvfrom返回成功指示,接着处理数据。 此过程称为轮询(polling)。这对CPU时间是极大的浪费。
I/O复用模型
调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。
信号驱动I/O模型
套接口启动信号驱动I/O, 并通过系统调用sigaction安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。
当数据报准备好被读时,就为该进程生成一个SIGIO信号。
随即可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也可以通知主循环,让它来读数据报。
异步I/O模型
让内核启动操作,并在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知用户。
信号驱动I/O:由内核通知我们何时可以启动一个I/O操作,
异步I/O模型:由内核通知我们I/O操作何时完成。
--------------------------------------------------------------------------------
select 函数
允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。
作为一个例子,我们可以调用函数select并通知内核仅在下列情况发生时才返回
集合{1,4,5}中的任何报述字准备好读
集合{2,7}的任何描述字准备好写
集合{1,4}中的任何描述字有异常条件待处理
已经过了10.2秒
通知内核我们对哪些描述字感兴趣(读、写或异常条件)以及等待多长时间。
描述字不受限于套接口:任何描述字(例如文件描述字)都可用select来测试。
select 定义
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
maxfdp1 : 描述字最大值
readset : 读描述字集
writeset : 写描述字集
exceptset : 异常条件的描述字集
timeout : 等待时间
readset, writeset和exceptset
让内核测试读、写和异常条件所需的描述字。
为这三个参数的每一个指定一个或多个描述字值
描述字集,是一个整数数组,每个数中的每一位对应一个描述字。
数组的第一个元素对应于描述字0-31,
数组户的第二个元素对应于描述字32—63。
例子:
fd_set rset; //定义描述字集数据类型
FD_ZERO (&rset); //对描述字集初始化
FD_SET(1, &rset); //打开描述字的第1位
FD_SET(4, &rset) // //打开描述字的第4位
......
FD_ISSET(4, &rest) //测试描述字的第4位
FD_CLR(4, &rset) // //关闭描述字的第4位
readset 套接口准备好读
套接口接收缓冲区中的数据字节数>=
套接口接收缓冲区低潮限度的当前值
连接的读这一半关闭(接收了FIN的TCP连接)
套接口是一个监听套接口旦已完成的连接数为非0。
有一个套接口错误待处理。
writeset 套接口准备好写
套接口发送缓冲区中的可用空间字节数大干等于套接口发送缓冲区低潮限度的
当前值,且或者(i)套接口已连接,或者(i)套接口不要求连接
连接的写这一半关闭。对这样的套接口的写操作将产生信SIGPIPE。
有一个套接口错误待处理。对这样的套接口的写操作将不阻塞且返回一个错误(一1)
exceptset异常条件待处理
如果一个套接口存在带外数据或者仍处于带外标记,那它有异常条件待处理。
带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。
最大描述字 maxfdp1
当select刚开始设计时,操作系统常对每个进程可用的最大描述字数上限作出
限制(4.2BSD的限制为31),select也就用相同的限制值。
unix版本对每个进程的描述字数根本不作限制 (仅受限于内存量和管理性限制),
#include <sys/types.h>
#DEFINE FD_SETSIZE 256
--------------------------------------------------------------------------------
str_cli 函数的修订版
服务器进程一终止客户就能马上得到通知
早期版本的问题就在于当套接口上发生了某些事件时,客户可能阻塞于fgets调用,
新版本则阻塞于select调用:等待标准输入,等待套接口可读。
对套接口的处理
对方TCP发送数据,套接口就变为可读且read返回大于0
对方TCP发送一个FIN(对方进程终止),套接口就变为可读且read返回0(文件结束)。
对方TCP发送一个RST (对方主机崩溃并重新启动),套接口变为可读且返回-1
#include "unp.h"
void str_cli(FILE *fp, int sockfd)
{ int maxfdp1; //最大描述字
fd_set rset; //描述字集
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset); //描述字集清零(空集)
for ( ; ; ) {
FD_SET(fileno(fp), &rset); //打开文件描述字的测试
FD_SET(sockfd, &rset); // //打开套接口描述字的测试
maxfdp1 = max(fileno(fp), sockfd) + 1; //获得最大描述字
Select(maxfdp1, &rset, NULL, NULL, NULL); //对是否可读进行测试
if (FD_ISSET(sockfd, &rset)) { //如果套接口可读
if (Readline(sockfd, recvline, MAXLINE) = = 0) //读入一行
err_quit(“str_cli: server termi. premat..”); //对方终止时退出
Fputs(recvline, stdout); //写到标准输出
}
if (FD_ISSET(fileno(fp), &rset)) { //如果标准输入可读
if (Fgets(sendline, MAXLINE, fp) == NULL) //读入一行
return; // 遇到^D时退出子程序
Writen(sockfd, sendline, strlen(sendline)); //写入套接口
}
}
}
--------------------------------------------------------------------------------
shutdown 函数
close有两个限制可由函数shutdown来避免:
close将描述字的访问计数减1,仅在此计数为0时才关闭套接口
shutdown可激发TCP的正常连接终止序列, 而不管访问计数。
close终止了数据传送的两个方向:读和写。
shutdown终止的数据传送的两个方向:读和写, 或其中任一方向:读或写
定义:
int shutdown( int sockfd, int howto) ;
howto选项:
SHUT_RD 关闭连接的读一半
SHUT_WR 关闭连接的写这一半
SHUT_RDWR 关闭连接读读和写
--------------------------------------------------------------------------------
str_cli函数(再修订)
void str_cli(FILE *fp, int sockfd)
{ int maxfdp1, stdineof ; fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
stdineof=0; FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0) FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0) {
if (stdineof == 1) return; /* normal termination */
else err_quit(“str_cli: server ERR. "); }
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue; }
Writen(sockfd, sendline, strlen(sendline));
} } }
--------------------------------------------------------------------------------
TCP回射服务器程序(修订版)
使用select来处理任意数目的客户的单进程程序
不为每个客户派生一个子进程,避免了创建一个新进程的所有开销。
监听套接口
服务器只维护一个读描述字集
描述字0、1和2分别被设置为标准输入、标准输出和标准错误输出
监听套接口的第一个可用的描述字是3。
与第一个客户建立连接
监听描述字变为可读,于是服务器调用accept。
由accept返回的新的已连接描述字将是4。
第一个客户终止与服务器的连接
客户TCP发送一个FIN,这使得服务器中的描述字4变为可读。
当服务器读此已连接套接口时,readline返回0。
关闭此套接口并相应地更新数据结构,数组元素client[0]]的值置为一1,
描述字集中的描述字4被置为0,maxfd的值没有改变。