1.Socket相关概念


    



     ~socket叫做“套接字”,用于描述ip地址和端口。是一个通信链的句柄。



     ~socket非常类似于电话插座



     ~在Internet上有很多这样的主机,这些主机一般运行了很多个服务软件,同时提供几种服务.每种服务都打开一个socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序).



     ~例如:http使用80端口 ftp使用21端口 smtp使用23端口



     两种类型



          一.流式Socket(STREAM):是一种面向连接的socket,针对于面向连接的TCP服务应用,安全,但是效率低



          二.数据报式Socket(DATAGRAM):是一种无连接的Socket,对应于无连接的UDP服务应用,不安全(丢失,顺序错乱,在接收端要分析重排及要求重发),但效率高





2.Socket一般应用模式(服务器和客户端)



   



   监听客户端请求,客户端, 连接套接字 



     服务端的Socket(至少两个)



          一个负责接收客户端连接请求



          每成功接收到一个客户端的连接便在服务端产生一个套接字



               ~为接收客户端连接时创建



               ~每一个客户端对应一个Socket



      客户端Socket



          客户端Socket



               必须制定连接服务端地址和端口



               通过创建一个Socket对象来初始化一个到服务器端的TCP连接



3.Socket通信基本流程



     服务端:



     1申请一个Socket



     2绑定到一个ip地址和一个端口



     3开启监听,等待接收连接



     客户端



     1申请一个socket



     2连接服务器(指明ip地址和端口号)



     !服务端接到连接请求后,产生一个新的socket(端口大于1024)与客户端建立连接并进行通讯,原监听socket继续听



    



4.服务器端



     创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据)



     获得文本框中的ip地址对象



     创建包含ip和port的网络节点对象



TextBox 
  .CheckForIllegalCrossThreadCalls = 
    
  false 
  ; 
    
  //关闭对文本框的跨线程操作
 
 

 
 
namespace 
     
   聊天程序
 
  
{
 
  
    
     
   public 
     
   partial 
     
   class 
     
   Form1 
     
   : 
     
   Form
 
  
    {
 
  
        
     
   public 
     
   Form1()
 
  
        {
 
  
            InitializeComponent();
 
  
            
     
   TextBox 
   .CheckForIllegalCrossThreadCalls = 
     
   false 
   ; 
     
   //关闭对文本框的跨线程操作
 
  
        }
 
  

 
  
        
     
   Thread 
     
   threadWatch = 
     
   null 
     
   ; 
   //负责监听的 线程
 
  
        
     
   Socket 
     
   socketWatch = 
     
   null 
     
   ; 
   //负责监听的 套接字
 
  

 
  
        
     
   private 
     
   void 
     
   btnListen_Click( 
   object 
     
   sender, 
     
   EventArgs 
     
   e)
 
  
        {
 
  
            
     
   // 创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据)
 
  
            socketWatch = 
     
   new 
     
   Socket 
     
   ( 
   AddressFamily 
   .InterNetwork, 
     
   SocketType 
   .Stream, 
     
   ProtocolType 
     
   .Tcp);
 
  
            
     
   //获得文本框中的ip地址对象
 
  
            
     
   IPAddress 
     
   address = 
     
   IPAddress 
     
   .Parse(txtbxIP.Text.Trim());
 
  
            
     
   //创建包含ip和port的网络节点对象
 
  
            
     
   IPEndPoint 
     
   endPoint = 
     
   new 
     
   IPEndPoint 
   (address, 
     
   int 
   .Parse(txtbxPort.Text.Trim()));
 
  
            
     
   // 负责监听的套接字 绑定到唯一的IP和端口上
 
  
            socketWatch.Bind(endPoint);
 
  
            
     
   //设置监听队列的长度,同时能够处理的连接最大数量,连接总数是没有上限的,取决于服务器的能力
 
  
            socketWatch.Listen(10);
 
  
            threadWatch = 
     
   new 
     
   Thread 
     
   (WatchConnecting);
 
  
            threadWatch.IsBackground = 
     
   true 
   ; 
     
   //设置为后台线程
 
  
            threadWatch.Start(); 
     
   //开启线程
 
  
            ShowMsg( 
     
   "服务器启动监听!" 
     
   );         
 
  
        }
 
  
        
     
   /// 
     
   <summary>
 
  
        
     
   /// 
     
   监听客户端请求的方法
 
  
        
     
   /// 
     
   </summary>
 
  
        
     
   void 
     
   WatchConnecting()
 
  
        {
 
  
            
     
   while 
     
   ( 
   true 
     
   ) 
   //持续不断的监听新的客户端的连接请求
 
  
            {
 
  
                
     
   //开启监听 客户端 连接请求 , 注意: Accept方法 会阻断当前的线程!
 
  
                
     
   Socket 
     
   sokConnection = socketWatch.Accept(); 
   //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
 
  
                ShowMsg( 
     
   "客户端连接成功!" 
     
   );
 
  
            }
 
  
        }
 
  

 
  
        
     
   void 
     
   ShowMsg( 
   string 
     
   msg)
 
  
        {
 
  
            txtbxMsg.AppendText(msg + 
     
   "\r\n" 
   );
 
  
        }
 
  
    }
 
  
}


5.客户端



private 
    
  void 
    
  btnLink_Click( 
  object 
    
  sender, 
    
  EventArgs 
    
  e)
 
 
        {
 
 
            
    
  //得到IPAddress
 
 
            
    
  IPAddress 
    
  address = 
    
  IPAddress 
    
  .Parse(txtbxIP.Text.Trim());
 
 
            
    
  //得到IPEndPoint
 
 
            
    
  IPEndPoint 
    
  endPoint = 
    
  new 
    
  IPEndPoint 
  (address, 
    
  int 
  .Parse(txtbxPort.Text.Trim()));
 
 
            
    
  //创建套接字
 
 
            
    
  Socket 
    
  socketClint = 
    
  new 
    
  Socket 
  ( 
    
  AddressFamily 
  .InterNetwork, 
    
  SocketType 
    
  .Stream, 
    
  ProtocolType 
  .Tcp);
 
 
            
    
  //套接字连接
 
 
            socketClint.Connect(endPoint);
 
 
        }



6.案例基础实现



     TCP下的stream操作,一个服务器端对应多个客户端,实现服务器端选择性发送信息,客户端发送信息到服务端:



----------------------------------------------------服务端--------------------------------------------




namespace 聊天程序服务端
{
    public partial class Form1 : Form
    {
       
        public Form1()
        {
            InitializeComponent();
            TextBox .CheckForIllegalCrossThreadCalls = false; //关闭对文本框的跨线程操作
        }
        Thread threadWatch = null ; //负责监听的 线程
        Socket socketWatch = null ; //负责监听的 套接字
        Socket sokConnection = null ; //
        private void btnListen_Click( object sender, EventArgs e)
        {
            // 创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据)
            socketWatch = new Socket ( AddressFamily.InterNetwork, SocketType .Stream, ProtocolType .Tcp);
            //获得文本框中的ip地址对象
            IPAddress address = IPAddress .Parse(txtbxIP.Text.Trim());
            //创建包含ip和port的网络节点对象
            IPEndPoint endPoint = new IPEndPoint(address, int .Parse(txtbxPort.Text.Trim()));
            // 负责监听的套接字 绑定到唯一的IP和端口上
            socketWatch.Bind(endPoint);
            //设置监听队列的长度,同时能够处理的连接最大数量,连接总数是没有上限的,取决于服务器的能力
            socketWatch.Listen(10);
            threadWatch = new Thread (WatchConnecting);
            threadWatch.IsBackground = true ; //设置为后台线程
            threadWatch.Start(); //开启线程
            ShowMsg( "服务器启动监听!" );         
        }
        //保存了服务端所有负责和客户端通信的套接字
        Dictionary <string , Socket> dict = new Dictionary < string, Socket >();
        //保存服务器所有连接的通信套接字 receive方法的线程
        Dictionary <string , Thread> dictThread = new Dictionary < string, Thread >();
        /// <summary>
        /// 监听客户端请求的方法
        /// </summary>
        void WatchConnecting()
        {
            while (true ) //持续不断的监听新的客户端的连接请求
            {
                //开启监听 客户端 连接请求 , 注意: Accept方法 会阻断当前的线程!
                sokConnection = socketWatch.Accept(); //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
                //向列表控件中 添加一个客户端的ip端口字符串,作为客户端的唯一标示
                listbx.Items.Add(sokConnection.RemoteEndPoint.ToString());
                //将于客户端通信的套接字对象 sokConnection 添加到 键值对集合中 并以客户端ip端口作为键
                dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection);
                ShowMsg( "客户端连接成功!" +sokConnection.RemoteEndPoint.ToString());
               
                //创建 通信线程
                ParameterizedThreadStart pts = new ParameterizedThreadStart(RecMsg);
                Thread tdRec = new Thread(pts);
                tdRec.IsBackground = true ;//设置为后台线程
                tdRec.SetApartmentState( ApartmentState .STA);
                tdRec.Start(sokConnection);
                dictThread.Add(sokConnection.RemoteEndPoint.ToString(), tdRec);
            }
        }
        /// <summary>
        /// 接收字符串
        /// </summary>
        void RecMsg(object sok)
        {
            Socket sokConn = sok as Socket;
            byte [] recByte = new byte[1024 * 1024 * 2];
            while (true )
            {
                int length = -1;
                try
                {
                    length = sokConn.Receive(recByte); //接收二进制数据字节流
                }
                catch (SocketException se)
                {
                    ShowMsg( "异常: " + se.Message + sokConn.RemoteEndPoint.ToString());
                    //从通信套接字字典中移除被中断连接的 通信套接字对象
                    dict.Remove(sokConn.RemoteEndPoint.ToString());
                    //从 通信线程 集合中 删除 被中断连接的 通信线程
                    dictThread.Remove(sokConn.RemoteEndPoint.ToString());
                    //从列表中删除 被中断连接的 ip:Port
                    listbx.Items.Remove(sokConn.RemoteEndPoint.ToString());
                    break ;
                }
                catch (Exception ex)
                {
                    ShowMsg( "异常: " + ex.Message);
                    break ;
                }
                //将接收到的流转换成string类型
                if (recByte[0] == 0)//判断 发送过来的数据 的第一个元素0 发送过来的是文本
                {
                    //此时 是将 数组 所有元素 都转换成字符串 而真正接受到 只有几个服务端的几个字节
                    string strMagRec = System.Text.Encoding .UTF8.GetString(recByte, 1, length-1);
                    ShowMsg(strMagRec);
                }
                else if (recByte[0] == 1)
                {
                    SaveFileDialog sfd = new SaveFileDialog();
                    if (sfd.ShowDialog() == System.Windows.Forms. DialogResult.OK)
                    {
                        string fileSavePath = sfd.FileName; //获得保存文件的路径
                        //创建文件流 然后让文件流来根据路径创建一个文件
                        using (FileStream fs = new FileStream (fileSavePath, FileMode .Create))
                        {
                            fs.Write(recByte, 1, length - 1);
                            ShowMsg( "文件保存成功:" + fileSavePath);
                        }
                    }
                }
            }
        }
        void ShowMsg(string msg)
        {
            txtbxMsg.AppendText(msg + "\r\n" );
        }
        /// <summary>
        /// 发送实现
        /// </summary>
        private void btnSend_Click( object sender, EventArgs e)
        {
            //必须选择发送对象
            if (string .IsNullOrEmpty(listbx.Text))
            {
                MessageBox .Show("请选择要发送的对象" );
            }
            else
            {
                string strMsg = txtbxMsgSnd.Text.Trim();
                if (string .IsNullOrEmpty(strMsg))
                {
                    ShowMsg( "发送文本不能为空!请输入!" );
                    return ;
                }
                //将要发送的字符串转成uft8对应的 数组
                byte [] arrMag = System.Text.Encoding .UTF8.GetBytes(strMsg);
                byte [] arrMagSend = new byte[arrMag.Length+1];
                Buffer .BlockCopy(arrMag, 0, arrMagSend, 1, arrMag.Length);
                string strClintKey = listbx.SelectedItem.ToString();
                try
                {
                    dict[strClintKey].Send(arrMagSend);
                }
                catch (SocketException se)
                {
                    MessageBox .Show("异常: " + se.Message);
                    return ;
                }
                ShowMsg( "发送出去:" + strMsg);
                //sokConnection.Send(arrMag);
            }
        }
        /// <summary>
        /// 群发
        /// </summary>
        private void btnSendAll_Click( object sender, EventArgs e)
        {
            string strMsg = txtbxMsgSnd.Text;
            byte [] arrMsg = System.Text.Encoding .UTF8.GetBytes(strMsg);
            byte [] arrMagSend = new byte[arrMsg.Length + 1];
            Buffer .BlockCopy(arrMsg, 0, arrMagSend, 1, arrMsg.Length);
            foreach (Socket sok in dict.Values)
            {
                try
                {
                    sok.Send(arrMagSend);
                }
                catch (SocketException se)
                {
                    MessageBox .Show("异常: " + se.Message);
                    return ;
                }
            }
            ShowMsg( "群发完毕:)" );
        }
    }
   
}





  



----------------------------------客户端----------------------------------------------






namespace 聊天程序客户端
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            TextBox .CheckForIllegalCrossThreadCalls = false;
        }
        Thread threadRec = null ; //创建接收线程
        Socket socketClint = null ; //创建接收套接字
       
        /// <summary>
        /// 连接服务器
        /// </summary>
        private void btnLink_Click( object sender, EventArgs e)
        {
            //得到IPAddress
            IPAddress address = IPAddress .Parse(txtbxIP.Text.Trim());
            //得到IPEndPoint
            IPEndPoint endPoint = new IPEndPoint(address, int .Parse(txtbxPort.Text.Trim()));
            //创建客户端套接字
            socketClint = new Socket ( AddressFamily.InterNetwork, SocketType .Stream, ProtocolType .Tcp);
            //套接字连接
            socketClint.Connect(endPoint);
            ShowMsg( "连接上服务器了!!哦也!" );
            threadRec = new Thread (RecMsg);
            threadRec.IsBackground = true ;
            threadRec.Start();
        }
       
        /// <summary>
        /// 接收方法
        /// </summary>
        void RecMsg()
        {
            //定义一个 接收用的 缓存区(2M字节数组)
            byte [] recByte = new byte[1024 * 1024 * 2];
            while (true )
            {
                int length = -1;
                try
                {
                    length = socketClint.Receive(recByte); //接收二进制数据字节流
                }
                catch (SocketException se)
                { //Exception是最顶级的异常父类,我们最好用最接近的异常,这里用SocketException
                    ShowMsg( "异常: " + se.Message);
                    break ;
                }
                catch (Exception ex)
                { //里面涉及装箱操作,会多一些操作
                    ShowMsg( "异常:" + ex.Message);
                }
                //将接收到的流转换成string类型
                if (recByte[0] == 0)//判断 发送过来的数据 的第一个元素0 发送过来的是文本
                {
                    //此时 是将 数组 所有元素 都转换成字符串 而真正接受到 只有几个服务端的几个字节
                    string strMagRec = System.Text.Encoding .UTF8.GetString(recByte, 1, length - 1);
                    ShowMsg(strMagRec);
                }
                else if (recByte[0] == 1)
                {
                    SaveFileDialog sfd = new SaveFileDialog();
                    if (sfd.ShowDialog() == System.Windows.Forms. DialogResult.OK)
                    {
                        string fileSavePath = sfd.FileName; //获得保存文件的路径
                        //创建文件流 然后让文件流来根据路径创建一个文件
                        using (FileStream fs = new FileStream (fileSavePath, FileMode .Create))
                        {
                            fs.Write(recByte, 1, length - 1);
                            ShowMsg( "文件保存成功:" + fileSavePath);
                        }
                    }
                }
            }
        }
        #region 在窗体文本框中显示字符串 - ShowMsg(string msg)
        /// <summary>
        /// 在窗体文本框中显示字符串
        /// </summary>
        /// <param name="msg"></param>
        void ShowMsg(string msg)
        {
            txtbxMsg.AppendText(msg + "\r\n" );
        }
        #endregion
        #region 发送文本-btnSend_Click
        /// <summary>
        /// 发送文本
        /// </summary>
        private void btnSend_Click( object sender, EventArgs e)
        {
            string strMsg = txtbxSndMsg.Text.Trim();
            //将要发送的字符串转成uft8对应的 数组
            byte [] arrMag = System.Text.Encoding .UTF8.GetBytes(strMsg);
            byte [] arrMagSend = new byte[arrMag.Length + 1];
            Buffer .BlockCopy(arrMag, 0, arrMagSend, 1, arrMag.Length); //拷贝
            try
            {
                socketClint.Send(arrMagSend); //发送
            }
            catch (SocketException se)
            {
                MessageBox .Show("异常: " + se.Message);
                return ;
            }
            ShowMsg( "发送出去:" + strMsg);
            //sokConnection.Send(arrMag);
        }
        #endregion
        #region 选择要发送的文件 - void btnOpenFile_Click
        /// <summary>
        /// 选择发送文件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOpenFile_Click( object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() == System.Windows.Forms. DialogResult.OK)
            {
                txtbxFilePath.Text = ofd.FileName;
            }
        }
        #endregion
        /// <summary>
        /// 向服务器发送文件
        /// </summary>
        private void btnSendFile_Click( object sender, EventArgs e)
        {
            //用文件流打开用户选择的文件
            //使用using是因为FileStream是挺占内存的类,使用using是为了更好地释放内存
            using (FileStream fs = new FileStream (txtbxFilePath.Text,FileMode .Open))
            {
                byte [] arrMsg = new byte[1024 * 1024 * 2]; //2M大小
                //将文件数据读到 数组
                int length = fs.Read(arrMsg, 0, arrMsg.Length);
                byte [] arrFileSend = new byte[length + 1];
                arrFileSend[0] = 1; //代表发送的是文件数据
                //块拷贝 将arrMsg数组的元素从0开始拷贝,拷贝到arrFileSend从1开始,拷贝length长度
                Buffer .BlockCopy(arrMsg, 0, arrFileSend, 1, length);
                //流拷贝 缺点 从0开始拷贝
                //arrMsg.CopyTo(arrFileSend, 0);
                try
                {
                    socketClint.Send(arrFileSend);
                }
                catch (SocketException se)
                {
                    MessageBox .Show("异常: " + se.Message);
                    return ;
                }
                ShowMsg( "已发送:" + txtbxFilePath.Text);
            }
        }
    }
}