c#学习笔记----------------Socket编程

学习笔记 / 2023-07-17 / 原文

一、什么是Socket

socket编程是网络常用的编程,我们通过在网络中创建socket关键字来实现网络间的进程通信。

1、网络间的进程如何通讯

首先要了解进程间的通讯方式:(win32 API)

参考博客:windows下进程间通信的,

Microsoft Win32应用编程接口(Application Programming Interface, API)提供了大量支持应用程序间数据共享和交换的机制,这些机制行使的活动称为进程间通信(InterProcess Communication, IPC),进程通信就是指不同进程间进行数据共享和数据交换。

  • 文件映射(Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。)
  • 共享内存(Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
  • 匿名管道(管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。匿名管道(Anonymous Pipe)是 在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。
  • 命名管道(命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
  • 邮件槽(邮件槽(Mailslots)提 供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。
  • 剪贴板(剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。
  • 动态数据交换(动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。
  • 对象连接和嵌入(应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据格式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服务。
  • 动态链接库(Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
  • 远程过程调用(Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。
  • NetBios函数(Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,其它应用程序最好不要使用NetBios函数来进行进程间通信。
  • Sockets(Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。
  • WM_COPYDATA消息(WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。
      WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。

两个进程不在同一台机器上他们的通讯就需要网络,一般都是通过tcp/ip进行网络通讯,他们就是送信的线路和驿站的作用

TCP/IP协议不同于iso的7个分层,它是根据这7个分层

TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中

应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等

传输层:TCP,UDP

网络层:IP,ICMP,OSPF,EIGRP,IGMP

数据链路层:SLIP,CSLIP,PPP,MTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的

 

 

有了网络之后就要唯一标识进程,就像写信都会有收件人和发件人,在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。

其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,

而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,

网络中的进程通信就可以利用这个标志与其它进程进行交互。

2、socket的定义

这样的话如果程序员需要编写一个有网络间进程通讯的软件,他不仅需要了解进程间通讯还需要了解网络间各种协议,

实现起来需要层层包装一条消息,是不是很麻烦,socket就应运而生了:Windows中的很多东西都是从Unix领域借鉴过来的,在Unix中,socket代表了一种文件描述符(在Unix中一切都是以文件为单位),而这里这个描述符则是用于描述网络访问的。

程序员可以通过socket来发送和接收网络上的数据。你也可以理解成是一个API。有了它,你就不用直接去操作网卡了,而是通过这个接口,这样就省了很多复杂的操作。

Socket所处的位置大概是下面这样的。

 我们可以发现socket就在应用程序的传输层和应用层之间,设计了一个socket抽象层,传输层的底一层的服务提供给socket抽象层,socket抽象层再提供给应用层

有了这些基本条件,我们就可以用它来访问网络了,具体操作如下:

  1.  确定本机的IP和端口
  2. 确定通讯协议(比如TCP 或者 UDP)
  3. 建立一个套接字
  4. 绑定本机的IP和端口
  5.  如果是TCP,因为是面向连接的,所以要利用Listen()方法来监听网络上是否有人给自己发东西;如果是UDP,因为是无连接的,所以来者不拒。
  6. TCP情况下,如果监听到一个连接,就可以使用accept来接收这个连接,然后就可以利用Send/Receive来执行操作了。而UDP,则不需要accept, 直接使用SendTo/ReceiveFrom来执行操作
  7. 如果不想继续发送和接收了,就不要浪费资源了。能close的就close吧

 

 下面将一一列出Socket常使用的对象(类和方法)

二、Socket编程常用对象和用法

开源C# Socket库推荐

1 命名空间

using System.Net;
using System.Net.Socket;

2 构造新的socket对象

public socket (AddressFamily addressFamily,SocketType sockettype,ProtocolType protocolType)

(1) AddressFamily 用来指定socket解析地址的寻址方案,Inte.Network标示需要ip版本4的地址,Inte.NetworkV6需要ip版本6的地址;

(2) SocketType 参数指定socket类型,Raw支持基础传输协议访问,Stream支持可靠,双向,基于连接的数据流;

  • Dgram(支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。 消息可能会丢失或重复并可能在到达时不按顺序排列。)
  • Raw(支持对基础传输协议的访问。)
  • Rdm(支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。)
  • Seqpacket(在网络上提供排序字节流的面向连接且可靠的双向传输。)
  • Stream(支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。)

(3) ProtocolType 表示socket支持的网络协议,如常用的TCP和UDP协议。

  • Tcp传输控制协议。
  • Udp用户数据报协议。
  • Raw原始 IP 数据包协议。
  • IP网际协议。
  • PupPARC 通用数据包协议。
  • IpxInternet 数据包交换协议。
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

 3定义主机对象

IPEndPoint类

public IPEndPoint(IPAddress address,int port)  

参数address可以直接填写主机的IP,如"192.168.2.1";

 //IPAddress(Byte[])    新实例初始化 IPAddress 类地址指定为 Byte 数组。;
            IPAddress ipAddress1 = new IPAddress(new byte[] { 151, 33, 86, 50 });
            //IPAddress(Int64)    新实例初始化 IPAddress 类地址指定为 Int64。
            IPAddress ipAddress2 = new IPAddress(0x2414188f);
            //Parse(String)     IP 地址将字符串转换为 IPAddress 实例
            IPAddress ipAddress3 = IPAddress.Parse("192.168.100.9");

            //IPEndPoint 表示主机地址和端口信息。
            IPEndPoint ipEndPoint = new IPEndPoint(ipAddress1, 8899);

            IPAddress[] ips = Dns.GetHostEntry("www.baidu.com").AddressList;

            ips = Dns.GetHostAddresses("www.cctv.com");

或者

    byte[] byts = IPAddress.Parse("192.168.2.1").GetAddressBytes();
            Array.Reverse(byts); // 需要倒置一次字节序
            long ipadress = BitConverter.ToUInt32(byts, 0); 
            IPEndPoint ipEndPoint2 = new IPEndPoint(ipadress, 8899);

利用DNS服务器解析主机,使用Dns.Resolve方法

public static IPHostEntry Resolve(string hostname)

参数:待解析的主机名称,返回IPHostEntry类值,IPHostEntry为Inte.Net主机地址信息提供容器,该容器提供存有IP地址列表,主机名称等。

Dns.GetHostByName获取本地主机名称

public static IPHostEntry GetHostByName(string hostname)

GetHostByAddress

public static IPHostEntry GetHostByAddress(IPAddress address)
public static IPHostEntry GetHostByAddress(string address)

 IPAddress:包含了一个IP地址[提供 Internet 协议 (IP) 地址]

//这里的IP是long类型的,这里使用Parse()可以将string类型数据转成IPAddress所需要的数据类型
IPAddress IP = IPAddress.Parse();

Encoding.ASCII:编码转换

Encoding.ASCII.GetBytes()    //将字符串转成字节
Encoding.ASCII.GetString()    //将字节转成字符串

4 端口绑定和监听

Connect()

建立远程主机的连接

        public void Connect(EndPoint remoteEP);

参数:IPEndPoint对象

Bind()

绑定一个本地的IP和端口号,参数是一个绑定了IP和端口号的IPEndPoint对象

       public void Bind(EndPoint localEP)

参数为主机对象 IPEndPoint

 Listen()

让Socket侦听传入的连接,参数为指定侦听队列的容量

public void Listen(int backlog)

参数为整型,表示监听挂起队列的最大值

accept()

接收连接并返回一个新的Socket,Accept会中断程序,直到有客户端连接

public socket accept()

返回值是socket对象

Close()

        public void Close()

关闭Socket,销毁连接

5 socket的发送和接收方法

发送数据:

public int Send(byte[] buffer)

参数为待发送数据的字节数组

public int Send(byte[],SocketFlags)

SocketFlags成员列表:

DontRoute不使用路由表发送,

MaxIOVectorLength为发送和接收数据的wsabuf结构数量提供标准值,

None 不对次调用使用标志,

OutOfBand消息的部分发送或接收,

Partial消息的部分发送或接收,

Peek查看传入的消息。

public int Send(byte[],int,SocketFlags)

参数多了字节数组的长度

public int Send(byte[],int,int,SocketFlags)

int的参数变成了开始发送的位置和字节数组的长度

NetWordStream类的Write方法

public override void write(byte[] buffer,int offset,int size)

参数:要发送的数组,开始发送位置,发送数组长度

NetWordStream类请看这篇文章:NetWordStream类

接收数据:

Socket类Receive方法

public int Receive(byte[] buffer) 
public int Receive(byte[],SocketFlags)
public int Receive(byte[],int,SocketFlags)  
public int Receive(byte[],int,int,SocketFlags)

NetworkStream类的Read方法

public override int Read(int byte[] buffer,int offset,int size)

参数:要读取的数组,开始读取位置,读取的数组长度

 

三、Socket套接字的TCP和UDP

TCP和UDP是常见的通信协议,我们需要指定套接字的一些参数,例如 IP 地址、端口号、协议等等,以确保通信能够顺利进行。

Socket 连接是一种重要的通信机制。它允许两个程序在不同计算机上进行实时通信,通过套接字的创建和使用来实现数据的传输。

打个比方说:我和小明通电话,我们一定要确认打通了才开始交流,打不通也会有专门的人告你怎么怎么不通;

电话打着打着,欸有一句没听清,可以让对面再说一次,这个是TCP的通信方式,Socket呢,Socket是电话机

我给小王寄信,反正信我是寄出去了,对面收没收到那我就不管了,要是一段时间内对面没回信,那就当对面没收到吧

这种通讯方式就是UDP,Socket呢,Socket这时候就是邮局

所以总结:socket是一种应用程序接口(API),用于在应用程序中访问TCP和UDP协议。通过socket,应用程序可以创建连接、传输数据,实现网络通信功能。

由于.NET框架通过UdpClient、TcpListener 、TcpClient这几个类对Socket进行了封装,使其使用更加方便

我们可以直接使用UdpClient、TcpListener 、TcpClient进行开发

1、UDP基本应用

与TCP通信不同,UDP通信是不分服务端和客户端的,通信双方是对等的。为了描述方便,我们把通信双方称为发送方和接收方。

下面是一个基于UDP的即时聊天小程序

 /// <summary>
        /// 显示数据使用异步的委托
        /// </summary>
        private delegate void InvokeDelegate();
        public MainForm()
        {
            InitializeComponent();
        }
        //全局变量
        Thread t = null;        //接收进程、接收显示进程
        string RecvData = null;    //接收的数据
        static Socket UdpClient = null;

        /// <summary>
        /// 窗体加载方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Load(object sender, EventArgs e)
        {
            //方法1:
            //CheckForIllegalCrossThreadCalls = false;
            this.tB_LocalIp.Text = GetLocalIp();
            this.tB_RemoteIp.Text = this.tB_LocalIp.Text;
            UdpClient = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            IPEndPoint localIPEndPoint = new IPEndPoint(IPAddress.Parse(this.tB_LocalIp.Text), Convert.ToInt16(this.tB_LocalPort.Text));
            UdpClient.Bind(localIPEndPoint);
            //开启线程
            t = new Thread(ReciveMsg);
            t.Start();
        }

        /// <summary>
        /// 获取本机IP地址
        /// </summary>
        public string GetLocalIp()
        {
            ///获取本地的IP地址
            string AddressIP = string.Empty;
            foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
            {
                if (_IPAddress.AddressFamily.ToString() == "InterNetwork")
                {
                    AddressIP = _IPAddress.ToString();
                }
            }
            return AddressIP;
        }
        /// <summary>
        /// 接收发送给本机ip对应端口号的数据报
        /// </summary>
        void ReciveMsg()
        {
            while (true)
            {
                    EndPoint point = new IPEndPoint(IPAddress.Any, 0);    //用来保存发送方的ip和端口号
                    byte[] buffer = new byte[1024];
                    int length = UdpClient.ReceiveFrom(buffer, ref point);//接收数据报
                    //led = Encoding.Default.GetString(buffer, 0, length);
                    //led_ctrl();
                    RecvData += "【from " + point + "】:" + Encoding.Default.GetString(buffer, 0, length);//Encoding.UTF8.GetString方法不能支持中文
                    RecvData += "\r\n";//接收完换行   
                   this.Invoke(new InvokeDelegate(DisplayReciveMsg));//方法2:

            }
        }
        /// <summary>
        /// 显示数据,并滑到最底
        /// </summary>
        public void DisplayReciveMsg()
        {
            this.rtB_RecvMsg.Text = RecvData;
            //让富文本框自动滑到最底行
            //让文本框获取焦点 
            this.rtB_RecvMsg.Focus();
            //设置光标的位置到文本尾 
            this.rtB_RecvMsg.Select(this.rtB_RecvMsg.TextLength-1, 0);
            //滚动到控件光标处 
            this.rtB_RecvMsg.ScrollToCaret();
        }

        private void btn_SendMsg_Click(object sender, EventArgs e)
        {
            /* 实列化Socket套接字对象
             * 参数: 
             *       AddressFamily(地址族) :InterNetwork ——> IP 版本 4 的地址
             *       SocketType(套接字类型):Dgram        ——> 数据报       
             *       ProtocolType(支持类型):UDP          ——> UDP协议  
             * 
             */
            Socket sSndMag = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            //获取UI中对方的IP地址
            IPAddress ipaddrRemote = IPAddress.Parse(this.tB_RemoteIp.Text);
            //IPEndPoint——>指定地址和端口号
            EndPoint edpRemote = new IPEndPoint(ipaddrRemote, Convert.ToInt16(this.tB_RemotePort.Text));
            //发送,发送前需要把发送的内容转成字节类型的
            sSndMag.SendTo(System.Text.Encoding.Default.GetBytes(this.rtB_SendMsg.Text), edpRemote);
            //关闭套接字
            sSndMag.Close();


        }


        private void btn_ClrSendMsg_Click(object sender, EventArgs e)
        {
            rtB_SendMsg.Text = "";
        }

        private void btn_ClrRecvMsg_Click(object sender, EventArgs e)
        {
            RecvData = "";
            rtB_RecvMsg.Text = "";
        }

下载链接:聊天小程序

总结如下:

tcp必须建立连接才可以进行通信

udp不需要建立通信

但是两者都需要一个监听来接收消息

 

四、Socket编程实现

 

五、根据Socket原理实现一个聊天器