学习中我坚信要知其然,也要知其所以然,不能只当API的搬运工。
过了一天我居然都有点忘了Socket双端连接发送的操作步骤了,在这里先复习一下。
客户端
1.创建Soeket对象。
2.socket对象连接。
3.进行你的操作。
服务端
1.创建监听对象,还是Socket类型
2.绑定Bind地址和端口号
3.开启监听
4.客户端连接时应答Accept
复习完后开始今天的学习,多人聊天室
使用异步代码+不断的委托,可以做到一个事情做完后继续等待下一个事情。
这样当有多个客户端请求连接时,就可以一个一个排好顺序Accept然后等待收数据。
服务器也多出来一个监听列表Clients,用来监听Clientstate。
当服务器收到消息时就会遍历列表给他们发送消息。
服务端代码:
foreach(ClientState s in clients.values)
{
s.socket.send(sendBytes);
}
ClientState是自定义的一个类,拥有一个Socket以及一个缓冲区。
class ClientState
{
public Socket socket;
public byte[] readBuff = new byte[1024];
}
Poll会检查Socket的状态其参数分别表示,等待回应的时间,以及当前Socket的状态。
read表示可读,就是当前的Socket有数据可以读取,及可以调用Receive。
write表示可写,就是反过来表示当前的Socket可以写入数据,就可以调用Send。
erro表示错误。
这里展示了一个客户端接收的代码演示
if (socket.Poll(0, SelectMode.SelectRead))
{
byte[] readBuff = new byte[1024];
int count = socket.Receive(readBuff);
string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
}
不难看出只有Ture就是Socket为可读的情况下才会执行读取的操作,相较于异步代码简洁了不少。
服务端这里的话
while(true){
//检查listenfd
if(listenfd.Poll(0, SelectMode.SelectRead)){
ReadListenfd(listenfd);
}
//检查clientfd
foreach (ClientState s in clients.Values){
Socket clientfd = s.socket;
if(clientfd.Poll(0, SelectMode.SelectRead)){
if(!ReadClientfd(clientfd)){
break;
}
}
}
//防止cpu占用过高
System.Threading.Thread.Sleep(1);
}
public static void ReadListenfd(Socket listenfd){
Console.WriteLine("Accept");
Socket clientfd = listenfd.Accept();
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
}
先初始化一个Socket类型的监听对象,
然后开始判断当对象有数据可读时,就代表有客户端进来了。
执行ReadListenfd,接收客户端,创建一个ClientState,并把ClientState加入到监听列表当中。
遍历客户端,当有客户端可读时就代表有客户端发送数据了,我们需要给其他客户端发消息
public static bool ReadClientfd(Socket clientfd){
ClientState state = clients[clientfd];
int count = clientfd.Receive(state.readBuff);
//客户端关闭
if(count == 0){
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return false;
}
//广播
string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
Console.WriteLine("Receive " + recvStr);
string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
foreach (ClientState cs in clients.Values){
cs.socket.Send(sendBytes);
}
return true;
}
这里会先判断客户端有没有关闭,如果没有就返回True并且发送数据,如果有关闭就是False并且要将他逐出列表。
这里因为foreach对列表操作了会报错,所以我们收到False就要及时打断循环。
这里我就想到一个问题,那我发到一半突然发现有人断开了,那后面的人岂不是都收不到了?
最后让程序挂起1毫秒防止卡死。
多路复用相较于上面的Poll他更近一步,做到了可以检测多个Socket的状态。
然后一次遍历找出符合条件的Socket修改列表。
while(true)
{
checkeRead.Clear();
checkeRead.Add(listenfd);
foreach (ClientState s in clients.Values)
{
checkeRead.Add(s.socket);
}
Socket.Select(checkeRead,null,null,1000);
foreach (var s in checkeRead)
{
if (s == listenfd)
ReadListenfd(s);
else
ReadClientfd(s);
}
}
这里主体就是先清空检查列表,然后把监听对象放进列表,然后遍历已经在监听的列表,放进检查列表,
然后Select,最后读取修改后的可Read的列表,如果监听对象可读说明有客户端进来了,如果不是说明有消息了,那么全体广播一下。
最后是这个挂起的1000毫秒,在1秒内如果如果没有任何可读信息,就会把检查列表设为空,然后返回。
我这里断点测试了下可以理解为就是程序被挂起了1000毫秒后再继续执行。
客户端与程序端差不多这里就不再实验了。
今天的学习到这里就结束了,基础打下来,后面就是实战了,后面就有机会再来了~