• C#实现局域网UDP广播--

    2016-11-21 14:49:40
    接收端:  Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一个Scoket协议  IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9095);...

     

    接收端:

               Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一个Scoket协议

                IPEndPoint iep 
    = new IPEndPoint(IPAddress.Any, 9095);//初始化一个侦听局域网内部所有IP和指定端口

                EndPoint ep 
    = (EndPoint)iep;

                socket.Bind(iep);
    //绑定这个实例

                
    while (true)
                
    ...{
                    
    byte[] buffer = new byte[1024];//设置缓冲数据流

                    socket.ReceiveFrom(buffer, 
    ref ep);//接收数据,并确把数据设置到缓冲流里面

                    Console.WriteLine(Encoding.Unicode.GetString(buffer2).TrimEnd('/u0000') + " " + DateTime.Now.ToString());
                        }

     

     

     

     

    发送端:

                Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一个Scoket实习,采用UDP传输

                IPEndPoint iep 
    = new IPEndPoint(IPAddress.Broadcast, 9095);//初始化一个发送广播和指定端口的网络端口实例

                sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 
    1);//设置该scoket实例的发送形式

                
    string request = "你好,TEST SEND!";//初始化需要发送而的发送数据

                
    byte[] buffer = Encoding.Unicode.GetBytes(request);

                sock.SendTo(buffer, iep);

                sock.Close();

     

    这样就可以实现对整个局域网内广播数据,和接收数据了.

    展开全文
  •  局域网即时通讯工具的设计与实现 PS:等有空了,放源码讲解吧。。。。 目录 局域网即时通讯工具的设计与实现 1.引言 1.1课题背景及意义 1.2发展现状 1.2.1产品同质化的现象严重 1.2.2产品定位不够精准 ...

     

                                  局域网即时通讯工具的设计与实现

    PS:等有空了,放源码讲解吧。。。。

    目录

    局域网即时通讯工具的设计与实现

    1.引言

    1.1课题背景及意义

    1.2发展现状

    1.2.1产品同质化的现象严重

    1.2.2产品定位不够精准

    1.3系统目标

    1.3.1显示主机名称及IP地址

    1.3.2收发文件、发送实时图像及发送录音

    1.3.3历史记录和下线刷新

    1.4运行环境及开发语言

    1.4.2开发运行环境的

    1.4.3系统开发语言

    2.相关开发技术

    2.1WPF(Windows Presentation Foundation)

    2.2C/S模式

    2.3多线程编程技术

    2.3.1进程与线程

    2.3.2多线程、线程池

    2.4TCP/UDP协议

    2.5Winsock网络编程

    3.系统需求分析

    3.1系统总体需求分析

    3.2系统流程图

    3.2.1发送端流程图

    3.2.2接收端流程图

    3.3系统可行性分析

    3.3.1技术可行性

    3.3.2经济可行性

    3.3.3操作可行性

    4.系统设计

    4.1系统总体设计

    4.2主界面的设计

    4.3文字聊天模块

    4.3.1文字聊天

    4.3.2文字聊天流程图

    4.4文件传输模块

    4.5截图模块

    4.6录音模块

    4.7历史记录模块

    4.7.1数据库设计

    4.7.2各种类型信息的记录

    4.7.3历史消息的显示界面

    5.总结和展望

    5.1总结

    5.2展望

    参考文献

     

     

    1.引言

     

    1.1课题背景及意义

    互联网的蓬勃兴起,促进了基于因特网的即时通讯技术的迅速发展。自1998年以来,即时通讯行业发展迅速,其功能日益丰富。如今即时通讯已经发展成一种综合化信息平台,实现终端联网的即时通讯服务[1]。即时通讯是在因特网上扩展会话和分散通信方式的实例[2]。但作为社交工具[3],部分即时通讯软件并不满足有特定需求的用户群体,如对内部网络和Internet的接入有严格限制的用户。部分主流即时通讯软件存在如下问题:①广告等无关信息的不断推送;②对Internet网络的极度依赖;③基本功能之外的扩展功能庞多[4]。而局域网即时通讯工具能有效地解决上述问题[5],有效地提高网络资源利用率。 

    1.2发展现状

    1.2.1产品同质化的现象严重

    腾讯QQ,最开始是通过模仿ICQ起步的。在随后的发展中,通过不断创新,逐步成长起来,甚至超越了ICQ已有的成就,在我国即时通讯市场占有率达到了第一。这种效应模式也被后来许多公司模仿,但部分公司通常会直接模仿,而不进行创新。所以目前,我国即时通讯市场上的软件同质化程度高[6]。

    1.2.2产品定位不够精准

    目前,我国的即时通讯软件大部分属于娱乐为主,功能为辅的类型。开发出的应用功能,基本上都是为娱乐服务的。由于市场化经济的刺激,使得很多通讯工具的功能为了体现新颖,目的是为了吸引用户下载使用。但是,由于目前即时通讯工具庞杂,各类新颖的功能也层出不穷,使得许多功能不适用于用户群体,导致功能虽然看上去新颖,但是明显觉得多余,甚至是由于某些新颖的功能,而让用户放弃使用[7]。

    1.3系统目标

     

    1.3.1显示主机名称及IP地址

     

    在程序启动时,使用Winsock获取本机主机名称及MAC地址,将信息封装到UDP数据包中,每两秒在局域网中广播一次。局域网内的其他主机通过解析广播包,获取到本机的名称、MAC地址和IP地址。然后在系统的主界面能够显示局域网中其它已经安装并运行该工具的主机名称和IP地址,使用MAC地址标记各主机的昵称。同时,可以和其它主机进行文字聊天。

    1.3.2收发文件、发送实时图像及发送录音

    任何一台主机可以和其它主机进行收发文件,包括但不限于WORD、PPT、ZIP等文件格式。其次,实现类似QQ的实时截图功能。通过获取整个屏幕图片,利用鼠标选取特定区域,向选定主机发送。同时,通过调用winmm.dll中的mciSendString方法实现录音功能,点击录制语音,可发送给选定的主机。

    1.3.3历史记录和下线刷新

    保留与其它主机的通信信息,通过将历史记转换成特定格式存储到数据库,以达到保存历史记录到本地的功能。在系统下次启动后,用户能查阅之前的历史记录,也可以选择清空历史记录。但清空记录并不删除已经接受的文件和截图。

    1.4运行环境及开发语言

     

    1.4.2开发运行环境的

     

    本系统是一个面向中小型企业机构,满足内部通讯需要的局域网即时通讯系统。开发此系统所需的基本软、硬件环境为:

    (1).操作系统:Windows XP 64bit、Windows 7 64bit及Windows 8等其它64位操作系统

    (2) 内存:2G以上内存

    (3) 处理器:Intel Pentium PIII或更高处理器。

    (4) 通讯网络:局域网

    1.4.3系统开发语言

    本系统使用C#语言在VS2012平台上开发。C#是基于C/C++的面向对象的编程语言。它的主要优点:一是具有优异的快速开发能力,二是支持面向对象的方法[8]。

     

    2.相关开发技术

     

    2.1WPF(Windows Presentation Foundation)

    WPF是为.NET框架设计的全新的软件界面显示系统。为了方便软件开发人员使用该系统的功能,微软提供了一个编程类库。这个编程类库具有以下四点特性:1、对用户界面功能更广泛的支持和融合。2、声明式脚本语言的引入与正式支持。3、更简单的安装与配置4、更新的文字排版支持[9]。

    同时,比较基于GDI的图形界面程序拥有以下优越的特性:1、显示图形的设备无关性。2、更高的画面精度。3、对界面重叠的更好的处理。4、WPF显示使用了硬件加速。5、通过脚本控制界面执行效果。6、WPF的可移植性[9]。

    2.2C/S模式

    直接或间接地通过网络协议与其他计算机进行通信,是网络编程的目的。其的基本模型是客户机/服务器模型(Client/Server Model,简称C/S模式)[10]。而C/S模式在实际应用操作过程中采取主动请求方式,在客户进程发出请求完全是随机的,同时,可能有若干个用户请求连接同一个服务器。这就要求服务器具有解决并发请求的能力[11]。

    2.3多线程编程技术

     

    2.3.1进程与线程

     

    在Windows系统中,系统具有同时运行若干个程序的能力,而每个运行中的程序就是一个进程。进程是操作系统结构的基础。线程是进程中的一个实体。线程之间的相互制约,会导致线程在运行中呈现出间断性[12]。

    2.3.2多线程、线程池

    通过运行单个程序来创建多个线程来完成各自的功能,从而提高CPU的利用率。但多线程具有明显缺点:1、线程数量越多,系统内存消耗越大。2、为了管理并协调这些线程,需要CPU花费更多的时间跟踪。3、当线程之间访问共享资源时,可能会导致不可预知的问题。

    而线程池,能弥补线程的部分缺点。通过有限固定的少量线程执行大量程序代码,从而减少创建与销毁线程带来的性能开销。通过设置最大并发线程数,解决线程之间访问共享资源的问题。

    2.4TCP/UDP协议

    UDP(User Datagram Protocol):用户数据包协议,是无连接、不可靠的传输层协议,提供尽力服务但不保证数据的完整性。但相比较于TCP一对一的连接,UDP能提高广播和多播服务。广播(Broadcastinig)使用广播地址255.255.255.255(需要指明接收者的端口号),可以将信息发送到在同一网络内的所有主机。同时为了避免未来瘫痪,路由器不会转发本地广播信息。多播,即对同一网络中部分主机进行了逻辑分组。在数据接收发送时,数据包在网络中传播,分组内的主机会对数据作出响应,而没有被划入这个分组的主机将在数据链路层丢弃这些数据包。

    TCP(Transmission Control Protocol):TCP是面向连接的通信协议,所以只能用于端到端的通讯。TCP层将包排序并进行错误检查,同时实现虚拟电路间的连接[13]。

    2.5Winsock网络编程

    在编程中涉及到网络访问控制的开发人员可直接引用System.Net.Sockets命名空间。该命名空间封装有Winsock(Windows Sockets) 接口的托管实现,。Socket是计算机进行通信的端口,是相关网络应用程序设计中的重要的概念[14]。

    在实际编程中,直接使用Socket类,可以获得更多的控制,可以使用不同的协议,不仅包括基于TCP或UDP的协议,还可以自定义自己的协议,更多地控制基于TCP或UDP的协议。接收数据的方法有两种:Socket类的Receive方法和NetworkStream的Read方法,发送数据也有对应的两种方法:Socket类的Send方法和NetworkStream的Wtire方法[15]。

    NetworkStream类能保证用户发送的数据,在不需要用户管理的情况下,自动地全部发送到TCP缓冲区,大大简化了编程工作,同时还提供了许多实用的属性和方法,所以在实际编程中,应用的较多。

     

    3.系统需求分析

     

    3.1系统总体需求分析

    本系统是一个局域网内的即时通讯工具,根据毕业设计需求,本系统将分为五个系统:1、简单文字聊天子系统,在局域网中选择同样安装本系统的用户,可以一对一发送简单的文字信息。2、文件传输子系统,将文件转化成文件比特流,传输到选定的用户,再转化还原成文件。3、仿QQ截图子系统,捕捉并显示整个屏幕的图像,通过相应的鼠标事件,可以选取图像的部分,发送给选定的用户。4、简单录音子系统,点击录制语音信息,可发送给选定的用户。5、历史信息存储子系统,记录与每个用户的聊天信息(包括文字信息、文件、截图),形成相应的记录存储在数据库SQLite中。本系统的功能类似飞鸽传书软件。在设计系统界面时,按照简洁、直观的特点安排,在编程时应充分考虑UDP(User Datagram Protocol)及TCP(Transmission Control Protocol的优缺点,实现局域网内用户之间沟通便捷,资源共享快速。

    3.2系统流程图

    3.2.1发送端流程图

     

    图3-1是发送端主机发送信息的流程图。在启动程序时,会根据局域网中的广播报文生成相应的用户主机列表。选择需要聊天的主机,根据传输的信息不同,转换成相应的比特流并封装,然后发送给目标主机。

                                                                               图3-1 发送端业务流程图

    3.2.2接收端流程图

    图3-2是接收端主机接收信息的流程图。在启动程序时,根据广播信息生成相应的用户主机列表。接收到数据并作分析处理。根据信息类型的不同,生成相应的记录并显示在对应的地方。其中文件信息,需要用户判断是否接受。若不接受,将舍弃。若接受,则做与其他类型相似的处理。

                                                                               图3-2 接收端业务流程图

    3.3系统可行性分析

    3.3.1技术可行性

     

    本系统是为方便局域网内部人员交流沟通开发的。具体设计实现涉及到C#编程、TCP/IP 协议及WinSock网络编程、多线程编程技术。而本人在之前的大学课程中均有所涉及,借此机会,更加深入地学习、理解。

    3.3.2经济可行性

    成本方面:由于本系统的设计与实现属于教学科研的一个环节,并且由指导老师义务指导。所以开发本系统的经济成本是低的。

    效益方面:开发一个合理的系统,可大大节约网络资源,曾经依赖Internet的接入传输的信息,现在使用本系统就可以在局域网传输,大大减少数据报跳转的路由数量。综合考虑以后,开发本系统的经济可行性高。

    ​​​​​​​3.3.3操作可行性

    本系统的目的是方便局域网内部人员。并且由于系统功能简单,其相应的界面简洁,用户只需通晓简单的电脑操作知识,就能自由地应用本软件。

     

    4.系统设计

     

    4.1系统总体设计

    本系统主要分为五个部分:1、简单文字聊天。2、文件传输。3、仿QQ截图。4、简单录音。5、历史记录。所以接下来将系统分为五大模块来介绍。

    本系统设计实现时,关键的部分在于如何在局域网中发送接收数据,并对接收的数据进行解析。在本系统中,发收数据是采用C/S结构,每个运行在电脑的系统即是客户端又是服务端。在本系统中,UDP用于发送上线下线的数据包,TCP用于发送文本、文件、截图的数据包。在系统启动时,会同时启动三个线程:

    1、用UDP广播方式广播本机上线信息:

    每两秒发送一个UDP广播包,封装的数据为,表示加入聊天系统的字符串“JOIN”,即自定义的数据类型,随后是本机用于网络通信的网卡的MAC地址,最后一部分是本机的主机名

    2、在本地所有IP地址的8001端口监听任意IP地址发出的UDP报文:

    按照自定义封装的数据类型解析。如果是“JOIN”类的数据包,按MAC地址查询数据库,检查是否有相应的昵称记录,有就返回,反之就将新用户插入数据库。在主界面显示出数据包分析得出的用户信息。如果是“LEAV”类数据包(当系统关闭时会发出一个“LEAV”类数据包),则说明有用户下线,在主界面移除数据包分析得出的用户信息。

    3、在本地所有IP地址的11000端口监听其它主机的TCP连接请求:

    设置监听最大连接个数为100.每接收到一个请求,都会新建一个套接字(用于连接发出这个请求的主机),并新开线程处理这个新连接的数据接收,原线程继续监听。所有对已接收数据的处理都封装在ChatSession类中(对数据包的处理主要都在ChatSession类的StartChat方法中)。数据包内容的开头设置字符表达数据的类型,“MESS”表示,是普通的文本信息转化成的数据。“FILE”表示,是文件转化成的数据。“PICT”表示,是图片转化成的数据。不同类型的数据将在功能模块里面详细说明。但,不管是怎样的数据都会生成相应的记录插入到数据库中。

    其中UDP广播通知上线的主要代码为:

    public void BroadCast(object state) {

                UdpClient udpClient = new UdpClient();

                IPEndPoint ep = new IPEndPoint(IPAddress.Broadcast, 8001);

                string message = "JOIN" + GetMacAddress() + (string)state;

                byte[] buff = Encoding.Default.GetBytes(message);

                while (!completed) {

                    udpClient.Send(buff, buff.Length, ep);

                    Thread.Sleep(2000);

                }

                udpClient.Close();

                return;

            }

    private static string GetMacAddress(){

                try{

                    string strMac = string.Empty;

                    ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");

                    ManagementObjectCollection moc = mc.GetInstances();

                    foreach (ManagementObject mo in moc){

                        if ((bool)mo["IPEnabled"] == true) {

                            strMac = mo["MacAddress"].ToString();

                            break;

                        }

                    }

                    moc.Dispose();

                    mc.Dispose();

                    return strMac;

                }

                catch{

                    return "unknown";

                }

            }

    其中UDP广播通知下线的主要代码为:

    public void Leave(string state) {

                UdpClient udpClient = new UdpClient();

                IPEndPoint ep = new IPEndPoint(IPAddress.Broadcast, 8001);

                string message = "LEAV" + state;

                byte[] buff = Encoding.Default.GetBytes(message);

                udpClient.Send(buff,buff.Length,ep);

                udpClient.Close();

                return;

            }

    4.2主界面的设计

    因为本系统的用户设计主要是针对用户在局域网内的日常活动,以及考虑到毕业设计的时间要求,所以本系统的界面较为简单,以便给用户提供一个上手快,理解简单的使用环境。为了方便用户选择单个用户发送信息,所以在主界面左侧设计了一个ListView,用于显示所有同一局域网内上线的用户,见图4-1,ListView的子项包括了用户名、IP地址及昵称(昵称存储在本地数据库)。为了方便用户辨别本机,将本机信息显示在界面上侧。

                                                                                        图4-1 主界面显示

     

           为了方便区分各个用户,添加了昵称功能。修改昵称的控件将显示在主界面的下侧。右击准备修改昵称的用户,可以编辑昵称,昵称将以MAC地址为主键存储在本地数据库,修改后在用户列表中实时刷新,系统下次启动还能显示。如图4-2.

                                                                                        图4-2  修改昵称

    点击任意一个用户,将出现与这个用户的聊天控件,控件将显示主界面的右侧,并显示这个用户发送给本机的未读信息。为了有条理的、直观地显示信息,设计了一个ListView用来显示。ListView的子项具有点击事件。

                                                                                      图4-3 聊天界面

    4.3文字聊天模块

    4.3.1文字聊天

     

    点击某个用户后,出现聊天控件,后台程序记录被点击的用户信息。同时,会启动一个新线程,不断查询数据库中是否有这个用户发送的未读信息。有就返回这些未读信息,将这些信息显示在当前显示消息的ListView中,并将这些未读信息更新为已都读。

           发送端:当需要向特定用户发送信息时,直接在右下的文本框中编辑,点击发送按钮,后台后台会获取文本框中的内容,将获取到的文本信息加上“MESS”的信息头部,再转换成比特数据。结合之前记录的用户信息,初始化套接字Socket为字符流方式的TCP通信,通过三次握手协议建立连接,实现点对点的数据传输,发送比特数据,关闭Socke连接,生成相应已读类型的记录更新到数据库,同时也将信息添加到显示信息的ListView中。

           接收端:通过协议建立连接,接收的数据包,调用ChatSession类的StartChat方法解析,如果是文本信息,生成标记为未读信息的记录插入到数据库中,接收完数据,关闭连接。文件、截图类信息,另行处理。

    因为一旦数据发送接收完就关闭连接,所以不需要考虑TCP数据包粘包的问题。

    ​​​​​​​4.3.2文字聊天流程图

    图4-4是文字聊天发送端流程图。用户在聊天列表中,点击接收信息的用户,后台会记录所选的用户。在文本框编辑需要发送的信息并点击“发送”按钮。将获取文本框内的信息并加上头部“MESS”,形成要发送的信息。通过编码将信息转换成比特流,在与目标主机建立连接后发送比特流,发送完毕则关闭连接。最后,形成相应的信息记录插入到数据库中。

                                                                               图4-4 文字聊天发送端流程图

    图4-4是文字聊天接收端流程图。在建立连接后,接收比特流数据。数据接收完,就关闭连接,分析比特流数据,得到文本信息。最后,生成对应的未读信息记录插入到数据库中。

                                                                               图4-5 文字聊天接收端流程图

    4.4文件传输模块

    系统的文件传输也要求尽量像文字聊天一样,简单易操作,同时充分发挥局域网的优势。其中,建立连接的部分和文字聊天中的一致。因为本系统中传输文件是有自定义的数据头部,实现带文件名的传输,因此接下来将着重介绍对文件的封装。

    点击用户,然后点击聊天控件中的发送文件按钮,将出现打开文件的对话框,如图4-5。选择文件并发送。后台获取文件路径,将对应文件使用FileStream类直接转换成文件流。使用自定义的FileNameHead类中GetBytes方法得到,图4-6中除文件流以外的比特部分。其中包括用来表示报文类型的“FILE”字段,文件名转化成比特流后的长度以及文件名的比特流,生成相应的已读类型的消息记录,更新到数据库及显示信息的ListView中。

                                                                               图4-5 打开文件对话框

           在图4-6中,除文件流以外部分的比特流,统一使用Unicode编码,文件名的比特流长度使用Int32类型的变量表示。所以FILE字段对应的比特流定长为8比特,文件名的比特流长度的比特流定长为 4比特,文件名的比特流及文件流不定长,但文件名的比特流长度可以通过解析前一个字段得到。因为一旦数据发送接收完就关闭连接,所以不需要考虑文件流长度不定的问题。

                                                                                            图4-6  TCP数据的内容

    主机接收文件数据时,会结合用户列表显示是哪个主机发送的,并询问是否接收,如图4-7。如果选“是”,随即出现保存文件对话框,从图4-8中,可看到文件名是已经设置好的。主机接收数据时,首先根据数据开头部分判断出是文件数据,再依据图4-6的格式进行解析,具体操作也封装在FileNameHead类中。生成相应的未读类型的消息记录,更新到数据库中。

                                                                                            图4-7 接收文件

                                                                                      图4-8 保存文件对话框

    其中FileNameHead实现的主要代码为:

    class FileNameHead{

            public static byte[] GetBytes(string FileName){

                byte[] fileNameByte = Encoding.Unicode.GetBytes(FileName);

                byte[] fileNameLengthForVauleByte = Encoding.Unicode.GetBytes(fileNameByte.Length.ToString());

                byte[] fileAttributeByte = new byte[fileNameByte.Length + fileNameLengthForVauleByte.Length];

                fileNameLengthForVauleByte.CopyTo(fileAttributeByte,0);

               fileNameByte.CopyTo(fileAttributeByte,fileNameLengthForVauleByte.Length);

                return fileAttributeByte;

            }

            public static int GetFileNameByteLength(byte[] FileNameHeadByte){

                byte[] fileNameLengthForVauleByte = new byte[4];

                Array.Copy(FileNameHeadByte, 8, fileNameLengthForVauleByte,0,4);

                string FileNameByteLength = Encoding.Unicode.GetString(fileNameLengthForVauleByte);

                int FileNameByteLengthNum = Convert.ToInt32(FileNameByteLength);

                return FileNameByteLengthNum;

            }

            public static string GetFileName(byte[] FileNameHeadByte, int FileNameByteLength){

                byte[] fileNameBytes = new byte[FileNameByteLength];

                Array.Copy(FileNameHeadByte, 12, fileNameBytes, 0, FileNameByteLength);

                string FileName = Encoding.Unicode.GetString(fileNameBytes);

                return FileName;

            }

        }

    4.5截图模块

    截图模块中的连接部分与文字聊天的相同,截图完成后,保存为图片文件,传输方式与文件传输模块的相同。因此接下来将着重介绍截图功能。

    点击用户,然后点击聊天控件中的发送文件按钮。启动名字为ScreenCut的WinFrom窗口。在窗体的加载事件中,通过调用Graphics类的CopyFromScreen方法获取到电脑屏幕的截图,并设置为窗体的背景图像和原图,然后出现相应的提示。如图4-9

                                                                                      图4-9 截图提示

    在ScreenCut窗口的后台代码中,主要监听鼠标的点击事件。需要监听的鼠标点击事件主要分为三个:

    1、鼠标按鈕已按下的事件

    当鼠标第一次点击时,判断是鼠标右键还是左键:右键则关闭当前窗口退出截图。左键,则判断,图片有没有出现用于截取部分图片的红色边框,有就记录鼠标位置,没有就记录鼠标下落的点,同时设置Rectangle类变量Rect的X、Y的值。

    2、鼠标移动事件。

    当鼠标移动时,判断图片有没有出现用于截取区域的红色边框。如果有边框,而且鼠标左键处于点击状态,比较上次位置的鼠标位置,计算偏移量,使用Graphics类的DrawRectangle方法,在原图的基础上重新绘制红色边框,并将新绘制的图片显示在窗口中。因为不断刷新重新绘制的原因,所以用户在使用时,感受不到卡顿,直观上会以为红色边框在随鼠标移动。如果没有出现过红色边框,但鼠标左键处于点击状态。同样需要不断绘制边框,但需要考虑鼠标目前所在的点,与左键下落的点之间的位置关系,从而设置变量Rect的宽度和高度。截图效果如图4-10

    3、鼠标双击事件。

    只需要对一种情况进行处理。如果是鼠标左键点击,并且是在红色边框内点击的,则使用Graphics类的CopyFromScreen方法截取对应部分区域的图片。然后将图片保存在系统程序所在的目录下的pic文件夹中(若不存在,将被创建),图片的文件名将以DateTime.Now.ToFileTime().ToString()的返回值命名。返回值是当前时间的长整型数字,这个数字是由年精准到微秒的。最后启动SendPic窗口,关闭当前窗口。

                                                                                     图4-10 截图效果展示

    在SendPic窗口,显示刚刚截取的图片,具体效果图如图4-11。若点击“发送”按钮。将调用自定义的ClassSendPic类发送图片,同时生成相应已读类型的记录更新到数据库,同时也将信息添加到显示信息的ListView中。接收信息的客户端,将生成标记为未读信息的记录插入到数据库中。

                                                                                图4-11 发送图片窗口展示

    4.6录音模块

    录音模块的非关键部分与之前模块的相似,包括建立连接和传输方式。接下来具体介绍录音功能的实现。

    在聊天控件中,点击录音按钮,将启动名为Recording的窗口,如图4-12。在窗体的加载事件中,初始化录音设备并开始录音,即窗体一启动,系统就开始录制语音。

    在窗体的后台代码中,使用DllImport导入winmm.dll中的mciSendString方法。之后的录音操作都设计到此方法。

                                                                                        图4-12 录音界面

    点击发送按钮,停止录音,并将已录制的语言信息保存为WAV文件。文件将保存在系统程序所在的目录下的rec文件夹中(若不存在,将被创建),文件名的命名方式与截图的命名方式相同。随后实例化ClassSendReco类,新建一个线程执行ClassSendReco类的SendReco方法,传输语言信息。

    点击取消按钮,系统将停止录音并关闭录音界面,不保存录制的语音文件。

    4.7历史记录模块

    历史记录模块的功能是将历史记录保存在本地,在本系统下次启动时后还能显示出来,用户也可以清空历史记录.在系统启动时,会检查是否存在数据库和表,若没有则创建。考虑系统的应用环境和项目要求,所以数据库采用嵌入式数据库SQLite。同时,本功能模块主要的部分是数据库的设计以及对数据库的操作。

    ​​​​​​​4.7.1数据库设计

    根据软件需求分析。为了良好、有效地记录历史信息,设计了一个包含2个表的数据库,分别为:

    用户昵称表:放置了2个属性,分别为(MAC地址,用户昵称)。

    聊天信息表:放置了7个属性,分别为(信息号,MAC地址,发言人,消息类型,消息内容,是否读过,消息时间)

    1. 用户昵称表

                                                                               表1 Nick表

    字段名称

    数据类型

    是否允许为空

    主键

    描述

    MacAdd

    varchar(40)

    通信网卡的物理地址

    Nickname

    varchar(40)

    -

    用户昵称

     

    1. 聊天信息表

                                                                                表1 Detail表

    字段名称

    数据类型

    是否允许为空

    主键

    描述

    Did

    INTEGER

    用来标记的ID,设置为自动增长

    Dmac

    varchar(40)

    -

    物理地址

    Dspeak

    varchar(30)

    -

    发言人

    Dtype

    INTEGER

    -

    消息类型

    Dcont

    varchar(150)

    -

    信息的内容,根据Dread的值而不一样

    Dread

    INTEGER

    -

    是否读过的标识

    Ddatetime

    datetime

    -

    默认设置为插入信息的时间

     

                                                                                图4-13 Detail表数据库显示

    4.7.2​​​​​​​各种类型信息的记录

    每一条插入到数据库Detail表的数据都具有以下7个:

    Did:每条信息的ID号,用于标记,设置为从1开始自动增长。

    Dmac:用于区分消息,表示这条消息是与网卡MAC地址为Dmac的主机的通信过程中产生的。

    Dspeak:记录在这次通信中,这条信息是哪个主机发送的。

    Dtype:用数字类型记录信息类型,0表示文本信息,1表示文件信息,2表示截图的图片信息,3表示录制的语音信息

    Dcont:根据Dtype的值而不同,Dtype为0时,存储文本信息内容,Dtype为1、2或3时,存储文件、截图或音频的文件路径

    Dread:用数字类型标记这条消息是否读过,0表示未读,1表示已读

    Ddatetime:设置时间默认为插入这条信息的时间

    ​​​​​​​4.7.3历史消息的显示界面

    点击用户,然后点击“历史记录”按钮,将启动一个新窗口显示与这个用户的聊天记录,具体显示效果,如图4-13. 窗口中有个ListView用于显示。点击“清空”按钮,将调用自定义的DetailDAL类中的DeleteOne方法,删除与当前用户的所有聊天记录。

                                                                                图4-14 历史消息记录的显示

     

    在消息记录的显示界面中,点击ListView的子项。若是文件信息,将打开文件所在的文件夹。若是截图的图片信息,将打开一个DisplayPic的WinForm窗口用于显示图片。若是语音信息,则使用MediaElement控件播放音频文件。如图4-14.聊天界面中,信息显示的ListView同样具有这样的功能。

                                                                                    图4-15 点击截图显示

     

    5.总结和展望

     

    5.1总结

    在此次毕业设计中,我使用了C#语言和VS2012开发工具开发了一个基于WINJDOWS平台的局域网即时通讯工具。该系统在提高网络资源利用率的同时,实现了文字聊天、文件传输、实时截图和语言发收的功能。通过这次实践,大大提高了我的实际动手能力, 同时也锻炼了我的资料查阅搜寻的能力。

    在这次设计中,我充分了解到了C#语言的优秀性能和VS2012在开发方面的高效。同时,我也认识到了学习的重要性。在查阅资料的工程中,从刚开始的盲目看WPF相关书籍,到中期有选择地看C#语言书籍和C#开发范例书籍,再到后期针对性地在收集资料。在实际实现的过程中,最初盲目觉得功能简单,同时高估了自己的能力,计划短时间内实现,导致未完成最初的计划安排。后来,通过反思,开始尝试逐步实现各个功能,并对每一项功能键都进行一个详细的过程安排。文字聊天中的发送端发送信息大致分成五步实现。这次毕业设计,不仅使我感受到了自己的不足,也是我具有初步的系统开发能力。

    5.2展望

    由于时间的仓促,同时也受限于个人的开发能力,本系统还存在一些技术上的问题,在今后的学习实践中,还可以进行不断的修改和完善。可改进的方面大致可以分为五。

    1、主界面与聊天界面的分离。由于功能是逐步实现的,在实现文字聊天功能时,没有历史记录功能,同时Socket绑定监听的原因,导致主界面与聊天界面在同一窗口中。最直接的结果是用户不能同时和多个用户聊天。

    2、文件传输进度的显示。在传输大文件时,用户不确定文件的传输进度,由此可能会产生不可预知的情况。

    3、截图的改善。在截取图片时不够直观,图片的质量也不高。

    4、音频的质量。因为使用WINDOWS API函数的原因,录取的音频质量不高,带有一定的噪音。

    5、添加新功能,包括但不限于语言通话、视频通话等。

     

    参考文献

    1. 宋嘉程.即时通讯软件应用研究[J]. 山东青年, 2017,8:8-9,13.
    2. Hans John Nielsen. Library communication outside a library context: instant messaging as library service [J]. New Library World, 2009, 110(5/6).
    3. Carol X.J.Ou, Robert M.Davison, Xuepan Zhong, Yi Liang. Empowering employees through instant messaging [J]. Information Technology & People2010, 23(2).
    4. 陆伟,林培榕,林姿琼. 企业即时通讯系统设计[J].软件导刊, 2015,10:94-96.
    5. 雷东升.网络即时通讯系统[J].计算机与现代化, 2008,4:51-53.
    6. 花鹏.分析即时通讯工具现状及发展方向[J].探索科学, 2016,5:49.
    7. 柳烨,谭敬德.QQ 即时通讯工具在大学生课程学习中的需求分析[J].湖南第一师范学院学报,2014,04:80-84.
    8. 于峰海. 面向再制造的工程机械创新设计研究[D].陕西科技大学,2014.
    9. 张晗雨.WPF全视角分析[M].北京:机械工业出版社,2008.12.
    10. 李红.基于C/S模式的SOCKET网络编程分析[J].现代商贸工业, 2009,9:259-260.
    11. 张晓明.C#网络通信程序设计[M].北京:清华大学出版社,2015.
    12. 吴艳平. C#多线程编程[J].电脑迷, 2017,15:92.
    13. 严谦,阳泳. 网络编程TCP/IP协议与socket论述[J].电子世界, 2016,8:68-68,70.
    14. 李红.基于C/S模式的SOCKET网络编程分析[J].现代商贸工业, 2009,9:259-260.
    15. Christian Nagel.C#高级编程:C# 6 &.NET CORE 1.0[M]. 北京:清华大学出版社, 2017.4
    展开全文
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq;//JObject的引用 ...using System.Net.Sockets
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    using Newtonsoft.Json.Linq;//JObject的引用 
    using System.Net;
    using System.Net.Sockets;//Socket的引用
    
    namespace Demo10
    {
        class Program
        {
            static void Main(string[] args)
            {
                //string hostname = Dns.GetHostName();//获取主机名
                //System.Net.IPAddress[] ListIP = Dns.GetHostAddresses(hostname);//获取主机信息列表
                //string LocalIP = ListIP[1].ToString();//获取主机IP
                UdpClient udpClient = new UdpClient();//实例化UDP套接字
                IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("255.255.255.255"), 8990);//定义UDP连接对象
                string sendString = "!{\"arg\":\"*\",\"nodeid\":\"*\",\"opcode\":\"FIND_CCU\",\"requester\":\"LS_CCU\"}$";//通信的交互数据
                byte[] sendData = Encoding.Default.GetBytes(sendString);//数据的编码转化
    
                udpClient.Send(sendData, sendData.Length, ipEndPoint);//数据发送
    
                for (int i = 1; i <= 10; i++)//一个Client多个Server返回的循环处理
                {
                    byte[] recvDataByte = udpClient.Receive(ref ipEndPoint);
                    string recvData = Encoding.Default.GetString(recvDataByte);
                    recvData = recvData.Replace("\n", "");
                    recvData = recvData.Replace("!", "").Replace("$", "");
                    JObject ObjJson = JObject.Parse(recvData);
                    Console.WriteLine(ObjJson["arg"]["ip"]);
                }
                Console.ReadLine();
            }
        }
    }
    

    展开全文
  • C# UDP通信 多路广播

    2020-06-21 21:16:31
    C# UDP通信,可以拿来学习下,希望对你们有帮助
  • 用Socket在局域网内进行广播

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

    也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                   

    服务器和客户机采用Socket编程。
    问题1:服务器进入侦听状态,但是此时客户端并不知道服务器的地址。我该如何做?

    问题2:我想使客户端先发出一个广播,服务器接受广播后给客户机发送自己的Ip等信息。
    接着再建立Socket通讯。这样对吗?

    问题3:是不是进行广播必须是数据报SOCK_DGRAM   

    问题4:是不是通讯的双方必须都是数据流或数据报?如果服务器是数据流SOCK_STREAM套接字,而客户机是数据报套接字就不能够通讯?

    1、用广播(或组播)方式,客户端不需道服务器的地址,初始化时用程序建立一个新的广播地址。
    2、用广播(或组播)方式,可直接收发数据。不用侦听。
    3、是;
    4、只要是数据就行。
    例子:

    //   MSGSocket.cpp   :   implementation   file // #include   "stdafx.h " //#include   "AV8Rcvr.h " #include   "MSGSocket.h " #ifdef   _DEBUG #define   new   DEBUG_NEW #undef   THIS_FILE static   char   THIS_FILE[]   =   __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// //   CMSGSocket CMSGSocket::CMSGSocket() { bForceNoLoopback   =   FALSE; bDataReceived   =   false; /*   Variable   defined   for   this   project.   Not   necessarily   part   of   CMsocket   */ number=0; for(int   i=0;i <40;i++) { ready[i]=false; } number=0; newfile=false; receivenumber=0; filename= " "; } CMSGSocket::~CMSGSocket() { } //   Do   not   edit   the   following   lines,   which   are   needed   by   ClassWizard. #if   0 BEGIN_MESSAGE_MAP(CMSGSocket,   CSocket) //{{AFX_MSG_MAP(CMSGSocket) //}}AFX_MSG_MAP END_MESSAGE_MAP() #endif //   0 ///////////////////////////////////////////////////////////////////////////// //   CMSGSocket   member   functions BOOL   CMSGSocket::CreateSocket(LPCTSTR   strGroupIP,   UINT   nGroupPort) { /*   Create   socket   for   receiving   packets   from   multicast   group   */ LeaveGroup(); //if(!Create(nGroupPort,   SOCK_DGRAM,   FD_READ|FD_WRITE))     //CAsyncSocket if(!Create(nGroupPort,   SOCK_DGRAM,   NULL))     //CSocket { AfxMessageBox( "建立连接时出错,检查该频道是否已被别的窗口占用! "); return   FALSE; } BOOL   bMultipleApps   =   TRUE; /*   allow   reuse   of   local   port   if   needed   */ SetSockOpt(SO_REUSEADDR,   (void*)&bMultipleApps,   sizeof(BOOL),   SOL_SOCKET); /*   Fill   m_saHostGroup_in   for   sending   datagrams   */ memset(&m_saHostGroup,   0,   sizeof(m_saHostGroup)); m_saHostGroup.sin_family   =   AF_INET; m_saHostGroup.sin_addr.s_addr   =   inet_addr(strGroupIP); m_saHostGroup.sin_port   =   htons((USHORT)nGroupPort); /*   Join   the   multicast   group   */ m_mrMReq.imr_multiaddr.s_addr   =   inet_addr(strGroupIP); /*   group   addr   */   m_mrMReq.imr_interface.s_addr   =   htons(INADDR_ANY); /*   use   default   */   if(setsockopt(m_hSocket,   IPPROTO_IP,   IP_ADD_MEMBERSHIP,   (char   FAR   *)&m_mrMReq,   sizeof(m_mrMReq))   <   0) { AfxMessageBox( "CreateReceivingSocket   failed "); return   FALSE; } return   TRUE; } void   CMSGSocket::OnReceive(int   nErrorCode) { ::SetActiveWindow(AfxGetApp()-> m_pMainWnd-> m_hWnd); //AfxMessageBox( "MSG收到数据! "); //return; int   nError   =   ReceiveFrom   (&msg_commanddata,sizeof(csock_data),   m_strSendersIP,   m_nSendersPort); if(nError   ==   SOCKET_ERROR) AfxMessageBox( "Error   receiving   data   from   the   host   group "); else { if   (!bForceNoLoopback   ||   (bForceNoLoopback   &&   !(m_strSendersIP   ==   m_strLocalIP   &&   m_nSendersPort   ==   m_nLocalPort))) { //lyksetdata1(3); //AfxMessageBox( "MSG收到数据! "); ::PostMessage(GetActiveWindow(   ),WM_COMMAND,WM_RECEIVEMSG,(LPARAM)0); } } CSocket::OnReceive(nErrorCode);   } BOOL   CMSGSocket::LeaveGroup() { if(setsockopt   (m_hSocket,   IPPROTO_IP,   IP_DROP_MEMBERSHIP,   (char   FAR   *)&m_mrMReq,   sizeof(m_mrMReq))   <   0) return   FALSE; Close(); //   Close   receving   socket return   TRUE; } /* BOOL   CMSGSocket::Send(const   void*   strMessage,   int   nSize) { //CString   str=strMessage; //AfxMessageBox(str); if(SendTo(strMessage,   nSize,   (SOCKADDR*)&m_saHostGroup,   sizeof(SOCKADDR),   0)   ==   SOCKET_ERROR) return   FALSE; else return   TRUE; } */ BOOL   CMSGSocket::Send(csock_data   m_data1) { if(SendTo(&m_data1,   sizeof(csock_data),   (SOCKADDR*)&m_saHostGroup,   sizeof(SOCKADDR),   0)   ==   SOCKET_ERROR) {         return   FALSE; } else { //AfxMessageBox( "MSGSend! "); return   TRUE; } } BOOL   CMSGSocket::TextSend(CString   str) { CString   st=str; st+= "/@&/ "; //BOOL   bo=Send(st,   st.GetLength()+1); return   0; } BOOL   CMSGSocket::GetMaker(void) { return   bDataReceived; } void   CMSGSocket::SetMaker(BOOL   da) { bDataReceived=da; //ready[number]==da; } void   CMSGSocket::Init(void) { bForceNoLoopback   =   FALSE; bDataReceived   =   false; /*   Variable   defined   for   this   project.   Not   necessarily   part   of   CMSGSocket   */ number=0; for(int   i=0;i <40;i++) { ready[i]=false; } } BOOL   CMSGSocket::SendFile(CString   filename) { return   0; } void   CMSGSocket::ReadFile() { } BOOL   CMSGSocket::SendData(SOCKET_STREAM_FILE_INFO   m_data1) { //AfxMessageBox( "send........ "); if(SendTo(&m_data1,   sizeof(SOCKET_STREAM_FILE_INFO),   (SOCKADDR*)&m_saHostGroup,   sizeof(SOCKADDR),   0)   ==   SOCKET_ERROR) { //AfxMessageBox( "send   false ");         return   FALSE; } else { //AfxMessageBox( "send   ok "); return   TRUE; } }


     

    #if   !defined(AFX_MSGSocket_H__257F140C_C139_4112_BACA_2C16C0F155B8__INCLUDED_) #define   AFX_MSGSocket_H__257F140C_C139_4112_BACA_2C16C0F155B8__INCLUDED_ #if   _MSC_VER   >   1000 #pragma   once #endif   //   _MSC_VER   >   1000 //   MSGSocket.h   :   header   file // ///////////////////////////////////////////////////////////////////////////// //   CMSGSocket   command   target class   CMSGSocket   :   public   CSocket { //   Attributes public: char   m_strBuffer[32768]; char   m_strBuffer1[32768]; char   m_strBuffer2[32768]; //   Receiving   buffer   for   the   packet   that   has   arrived SOCKADDR_IN   m_saHostGroup; //   SOCKADDR   structure   to   hold   IP/Port   of   the   Host   group   to   send   data   to   it ip_mreq   m_mrMReq; //   Contains   IP   and   interface   of   the   host   group UINT   m_nSendersPort; //   Holds   Port   No.   of   the   socket   from   which   last   packet   was   received CString   m_strSendersIP; //   Hold   IP   of   the   socket   from   which   the   last   packet   was   received UINT   m_nLocalPort; //   Ephemeral   port   number   of   the   sending   port CString   m_strLocalIP; //   IP   Address   of   the   local   host   or   your   machine BOOL   bForceNoLoopback; //   If   interface   does   not   support   lopback   and   the   service   is   required,   the   bool   is   set   to   true BOOL   bDataReceived; BOOL   LeaveGroup(); //BOOL   Send(const   void*,   int); BOOL   Send(csock_data   m_data1); BOOL   CreateSocket(LPCTSTR,   UINT); BOOL   ready[40]; int   number; CString   text; //////////////// csock_data   msg_commanddata; BOOL   newfile; DWORD   fileID; DWORD     receivenumber; CString   filename; BOOL   GetMaker(void); void   SetMaker(BOOL   da); void   ReadFrom(void); void   Init(void); BOOL   TextSend(CString   text); BOOL   SendFile(CString   filename); void   ReadFile(void); BOOL   SendData(SOCKET_STREAM_FILE_INFO   m_data1); ///////////////////// //////////////// HINSTANCE   glib; LYKGETDATA   lykgetdata1; LYKSETDATA   lyksetdata1; //   Operations public: CMSGSocket(); virtual   ~CMSGSocket(); //   Overrides public: //   ClassWizard   generated   virtual   function   overrides //{{AFX_VIRTUAL(CMSGSocket) public: virtual   void   OnReceive(int   nErrorCode); //}}AFX_VIRTUAL //   Generated   message   map   functions //{{AFX_MSG(CMSGSocket) //   NOTE   -   the   ClassWizard   will   add   and   remove   member   functions   here. //}}AFX_MSG //   Implementation protected: }; ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} //   Microsoft   Visual   C++   will   insert   additional   declarations   immediately   before   the   previous   line. #endif   //   !defined(AFX_MSGSocket_H__257F140C_C139_4112_BACA_2C16C0F155B8__INCLUDED_) 用法是: void   CMainFrame::SendMSG(int   pcommand,int   pmsg) { if(!MSG_Socket.CreateSocket( "234.5.6.7 ",   206)) AfxMessageBox( "建立网络连接出错! "); //AfxMessageBox( "SendMSG "); //return; //AfxMessageBox( "aa "); ::memset(&msg_commanddata,0,sizeof(csock_data)); msg_commanddata.command=pcommand; msg_commanddata.serial=pmsg; POINT   pt; GetCursorPos(&pt); msg_commanddata.mousex=pt.x; msg_commanddata.mousey=pt.y; ////////////// char   ch[128];         ::gethostname(ch,100); hostent*   tent=::gethostbyname(ch); msg_commanddata.IP[0][0]=(byte)tent-> h_addr[0]; msg_commanddata.IP[0][1]=(byte)tent-> h_addr[1]; msg_commanddata.IP[0][2]=(byte)tent-> h_addr[2]; msg_commanddata.IP[0][3]=(byte)tent-> h_addr[3]; /////////////////////////////////////////// if(!MSG_Socket.Send(msg_commanddata)) { // for(int   i=0;i <3;i++) { //Sleep(100); if(!MSG_Socket.Send(msg_commanddata)) { //AfxMessageBox( "send   data   failed ");                         //return; } else {                                   //AfxMessageBox( "send   data   failed "); } } } else { } //AfxMessageBox( "send   end "); //return; } 


     

     

    //发送端程序#include <stdio.h>#include <winsock.h>int main(int argc, char* argv[]){    WSADATA wsaData;          //指向WinSocket信息结构的指针    SOCKET sockListener;    SOCKADDR_IN sin,saUdpServ;    BOOL fBroadcast = TRUE;    char sendBuff[1024];    int nSize;    int ncount=0// 初始化winsock库,使用socket的前提    if(WSAStartup(MAKEWORD( 1, 1 ), &wsaData )!=0)//进行WinSocket的初始化    {        printf("Can't initiates windows socket!Program stop.\n");//初始化失败返回-1        return -1;    } // 创建socket    sockListener=socket(PF_INET,SOCK_DGRAM,0); // 打开广播选项,是socket可以广播消息    setsockopt ( sockListener,SOL_SOCKET,SO_BROADCAST, (CHAR *)&fBroadcast,sizeof ( BOOL )); // 将socket绑定到本地端口    sin.sin_family = AF_INET;    sin.sin_port = htons(0);    sin.sin_addr.s_addr = htonl(INADDR_ANY);    if(bind( sockListener, (SOCKADDR *)&sin, sizeof(sin))!=0)    {        printf("Can't bind socket to local port!Program stop.\n");//初始化失败返回-1        return -1;    } // 设定广播的目的端口    saUdpServ.sin_family = AF_INET;    saUdpServ.sin_addr.s_addr = htonl ( INADDR_BROADCAST );    saUdpServ.sin_port = htons (7001);//发送用的端口,可以根据需要更改    nSize = sizeof ( SOCKADDR_IN );    while(1)    {  // 广播消息        sprintf(sendBuff,"Message %d",ncount++);        sendto ( sockListener,sendBuff,            lstrlen (sendBuff),            0,            (SOCKADDR *) &saUdpServ,            sizeof ( SOCKADDR_IN ));        printf("%s\n",sendBuff);    } return 0;}//接收#include <stdio.h>#include <winsock.h>#include <conio.h>int main(int argc, char* argv[]){    WSADATA wsaData;          //指向WinSocket信息结构的指针    SOCKET sockListener;    SOCKADDR_IN sin,saClient;    char cRecvBuff[1024];    int nSize,nbSize;    int iAddrLen=sizeof(saClient);    if(WSAStartup(MAKEWORD( 1, 1 ), &wsaData )!=0)//进行WinSocket的初始化    {        printf("Can't initiates windows socket!Program stop.\n");//初始化失败返回-1        return -1;    } // 绑定到7001端口,以监听来自网络的数据    sockListener=socket(AF_INET, SOCK_DGRAM,0);    sin.sin_family = AF_INET;    sin.sin_port = htons(7001);//发送端使用的发送端口,可以根据需要更改    sin.sin_addr.s_addr = htonl(INADDR_ANY);    if(bind( sockListener, (SOCKADDR FAR *)&sin, sizeof(sin))!=0)    {        printf("Can't bind socket to local port!Program stop.\n");//初始化失败返回-1        return -1;    }    while(1)    {        nSize = sizeof ( SOCKADDR_IN );  // 接受消息        if((nbSize=recvfrom (sockListener,cRecvBuff,1024,0,(SOCKADDR FAR *) &saClient,&nSize))==SOCKET_ERROR)        {            printf("Recive Error");            break;        }        cRecvBuff[nbSize] = '\0';        printf("%s\n",cRecvBuff);          }    return 0;}

    广播

    广播是指在一个局域网中向所有的网上节点发送信息。这是UDP连接的一种

    广播有一个广播组,即只有一个广播组内的节点才能收到发往这个广播组的信息。什么决定了一个广播组呢,就是端口号,局域网内一个节点,如果设置了广播属性并监听了端口号A后,那么他就加入了A组广播,这个局域网内所有发往广播端口A的信息他都收的到。在广播的实现中,如果一个节点想接受A组广播信息,那么就要先将他绑定给地址和端口A,然后设置这个socket的属性为广播属性。如果一个节点不想接受广播信息,而只想发送广播信息,那么不用绑定端口,只需要先为socket设置广播属性后,向广播地址INADDR_BROADCAST的A端口发送udp信息即可。详细的程序实现如下:

    1.初始化

        WSAStartup(MAKEWORD(2,2),&wsad);

    2.创建一个UDP的socket
        s=socket(AF_INET,SOCK_DGRAM,0);

    3.如果这个socket希望收到信息,则需要绑定地址和这组广播的端口号,如果只是希望发送广播信息,则不需要这步

        SOCKADDR_IN udpAdress,sender;
        int senferAddSize=sizeof(sender);
        udpAdress.sin_family=AF_INET;
        udpAdress.sin_port=htons(11114);
        udpAdress.sin_addr.s_addr=inet_addr("10.11.131.32");
        bind(s,(SOCKADDR*)&udpAdress,sizeof(udpAdress));

    //这样这个节点即可收到局域网内所有发往端口11114的广播信息

    4.设置socket的属性为广播
        bool optval=true;
        setsockopt(s,SOL_SOCKET,SO_BROADCAST,(char*)&optval,sizeof(bool));

    5.下面就可以使用recvfrom或sendto来收发广播信息了

    这里是接受,这是一个阻塞操作
                ret=recvfrom(s,data,1000,0,(SOCKADDR*)&sender,&senferAddSize);

    这里是像该广播组发送信息,注意发送的地址为广播地址INADDR_BROADCAST,端口号为改组广播的端口号11114

        SOCKADDR_IN dstAdd;
        dstAdd.sin_family=AF_INET;
        dstAdd.sin_port=htons(11114);
        dstAdd.sin_addr.s_addr=INADDR_BROADCAST;
        sendto(s,data(),totalbyte,0,(SOCKADDR*)&dstAdd,sizeof(SOCKADDR));

    多播

    多播与广播不同,多播是指一条信息向局域网内有限几个节点传递,而广播是不管某个节点是否在制定组内,都会向这个节点发送广播信息,容易造成网络负担严重。

    多播的实现是靠多播组,在局域网内,一个多播地址唯一的定义了一个多播组(端口号任意),可以使用的多播地址是有规定的,从224.0.0.0—239.255.255.255之间,但是其中的一些地址不能用,是用作特殊用途的:224.0.0.0 –224.0.0.2  224.0.1.1  224.0.0.9 224.0.1.24。一个节点如果想接受自某个多播组或向某个多播组发送信息,必须首先加入多播组,然后给予UDP发送。下面是详细的代码实现。

    1.初始化

        WSAStartup(MAKEWORD(2,2),&wsad);

    2.这里传建一个用于多播通信的socket,注意这个socket的参数为设置成多播
        s=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,WSA_FLAG_MULTIPOINT_C_LEAF|WSA_FLAG_MULTIPOINT_D_LEAF|WSA_FLAG_OVERLAPPED);

    3.将socket绑定到一个本地地址、端口,和广播不同,在多播中,无论是发送还是接收端都必须绑定一个本地地址,这个地址就是多播通信时处理信息的端口
        udpAdress.sin_family=AF_INET;
        udpAdress.sin_port=htons(22222);
        udpAdress.sin_addr.s_addr=inet_addr("10.11.131.32");
        bind(s,(SOCKADDR*)&udpAdress,sizeof(udpAdress));

    4.定义多播组的地址
        multiCastGroup.sin_family=AF_INET;
        multiCastGroup.sin_port=htons(1111);此处端口任意,每个节点的可以设置成不同的
        multiCastGroup.sin_addr.s_addr=inet_addr("224.0.0.3"); 此处需使用上面规定地址段内的多播地址

    5.加入这个多播组。注意这里的函数返回了一个socket,这个socket不负责通信,只是在脱离多播组时使用

        SOCKET sockM=WSAJoinLeaf(s,(SOCKADDR*)&multiCastGroup,sizeof(multiCastGroup),NULL,NULL,NULL,NULL,JL_BOTH);

    6.下面使用recvfrom接受多播信息,或者使用sendto发送多播信息  

    ret=recvfrom(s,data,1000,0,(SOCKADDR*)&sender,&senferAddSize);

    sendto(s,data(),totalbyte,0,(SOCKADDR*)&multiCastGroup,sizeof(multiCastGroup));

    7.最后关闭清理
        closesocket(sockM);
        closesocket(s);
        WSACleanup();

    其他:

    1)在多播组中,默认情况下一个发出多播信息的节点也会收到自己发送的信息,这称为多播回环,可以关闭多播回环:

    bool val=false;

    setsocket(s,IPPROTO_IP,IP_MULTICAST_LOOP,(char*)val,sizeof(val));

    2)在多播时,通常要设置适当的TTL(TTL的值是多少,那么多播信息就可以经过多少路由器,每经过一个路由器,TTl的值自动减1):

    int val=3;

    setsocket(s,IPPROTO_IP,IP_MULTICAST_TTL,(char*)val,sizeof(int));

     

     ////////////////////////////////////////////////////////////////////////// // UDPServer.cpp #include <stdio.h>#include <WINSOCK2.H> #pragma comment(lib,"WS2_32.lib")#define BUF_SIZE    64  int main(void) {      WSADATA wsd;      SOCKET  s;      int     nRet;       // 初始化套接字动态库      if(WSAStartup(MAKEWORD(2,2),&wsd) != 0)      {           printf("WSAStartup failed !\n");           return 1;      }       // 创建套接字      s = socket(AF_INET,SOCK_DGRAM,0);      if(s == INVALID_SOCKET)      {           printf("socket() failed ,Error Code:%d\n",WSAGetLastError());           WSACleanup();           return 1;      }       SOCKET      socketSrv = socket(AF_INET,SOCK_DGRAM,0);      SOCKADDR_IN addrSrv;      SOCKADDR_IN addrClient;      char        buf[BUF_SIZE];      int         len = sizeof(SOCKADDR);       // 设置服务器地址      ZeroMemory(buf,BUF_SIZE);      addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);      addrSrv.sin_family = AF_INET;      addrSrv.sin_port = htons(5000);       // 绑定套接字      nRet = bind(socketSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));      if(SOCKET_ERROR == nRet)         {              printf("bind failed !\n");              closesocket(s);              WSACleanup();              return -1;         }      // 从客户端接收数据      nRet = recvfrom(socketSrv,buf,BUF_SIZE,0,(SOCKADDR*)&addrClient,&len);      if(SOCKET_ERROR == nRet)         {              printf("recvfrom failed !\n");              closesocket(s);              WSACleanup();              return -1;         }      // 打印来自客户端发送来的数据      printf("Recv From Client:%s\n",buf);       // 向客户端发送数据      sendto(socketSrv,"UDP Hello World !",sizeof("UDP Hello World !"),0,(SOCKADDR*)&addrClient,len);      closesocket(s);      WSACleanup();      return 0; }


     

     ////////////////////////////////////////////////////////////////////////// // UDPClient.cpp #include <stdio.h>#include <WINSOCK2.H> #pragma comment(lib,"WS2_32.lib")#define BUF_SIZE    64  int main(void) {      WSADATA wsd;          SOCKET  s;       // 初始化套接字动态库      if(WSAStartup(MAKEWORD(2,2),&wsd) != 0)      {           printf("WSAStartup failed !\n");           return 1;      }       // 创建套接字      s = socket(AF_INET,SOCK_DGRAM,0);      if(s == INVALID_SOCKET)      {           printf("socket() failed, Error Code:%d\n",WSAGetLastError());           WSACleanup();           return 1;      }       char        buf[BUF_SIZE];    // 接受数据      SOCKADDR_IN servAddr;         // 服务器套接字地址      SOCKET      sockClient = socket(AF_INET,SOCK_DGRAM,0);      int         nRet;     ZeroMemory(buf,BUF_SIZE);      strcpy(buf,"UDP Hello World !");       // 设置服务器地址      servAddr.sin_family = AF_INET;      servAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.254");      servAddr.sin_port = htons(5000);       // 向服务器发送数据      int nServAddLen = sizeof(servAddr);      if(sendto(sockClient,buf,BUF_SIZE,0,(sockaddr *)&servAddr,nServAddLen) == SOCKET_ERROR)      {           printf("recvfrom() failed:%d\n",WSAGetLastError());           closesocket(s);           WSACleanup();           return 1;      }      nRet = recvfrom(sockClient,buf,BUF_SIZE,0,(sockaddr *)&servAddr,&nServAddLen);      if(SOCKET_ERROR == nRet)         {              printf("recvfrom failed !\n");              closesocket(s);              WSACleanup();              return -1;         }       // 打印来自服务端发送来的数据      printf("Recv From Server:%s\n",buf);      closesocket(s);      WSACleanup();      return 0; }


     

     本文讲述了SOCKADDR 与 SOCKADDR_IN 的区别与联系。已经里面涉及的结构体 联合体 等的一些细节问题。这个是一个很基础的问题,但是很多人都是似是而非的理解着!下面详解了这个谜团!

    -----------------------------------------------------------------------------------------------------------------------

    struct sockaddr {
            unsigned short sa_family; //    地址族, AF_xxx               AF_INET 不涉及转序的问题
            char sa_data[14];    // 14字节的协议地址 网络字节顺序的
        };
      
    上面是通用的socket地址,共16个字节!

    具体到Internet socket,用下面的结构,二者可以进行类型转换
      
    struct sockaddr_in {
            short int sin_family; /* 地址族 */
            unsigned short int sin_port; /* 端口号 */
           struct in_addr sin_addr; /* Internet地址 */
            unsigned char sin_zero[8]; /* 与struct sockaddr一样的长度 */ 16个字节
        };
      
        ---------------------------struct in_addr 就是32位IP地址---------------------------------
    第一种表示方式:  
        struct in_addr {
            unsigned long s_addr;
        };

    第二种表示方式:
    struct in_addr

       union
    {   
         struct { u_char s_b1,s_b2,s_b3,s_b4;} S_un_b;   
         struct { u_short s_w1,s_w2;} S_un_w;   
         u_long S_addr;
    } S_un;
    };

    利用u_long htonl(u_long hostlong);将主机字节序转换为TCP/IP网络字节序.
    利用u_short htons(u_short hostshort);将主机字节序转换为TCP/IP网络字节序.

    inet_addr()是将一个点分制的IP地址(如192.168.0.1)转换为上述结构中需要的32位IP地址(0xC0A80001)。

    通常的用法是:
    SOCKET sockfd;
    struct sockaddr_in my_addr;   //SOCKETADDR_IN my_addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 做一些错误检查! */

    my_addr.sin_family = AF_INET; /* 主机字节序 */
    my_addr.sin_port = htons(MYPORT); /* short, 网络字节序 */

    //有两种方式 对应上面 in_addr 的两种方式
    my_addr.sin_addr.s_addr = inet_addr("192.168.0.1");
    //my_addr.sin_addr.S_un.s_addr = inet_addr("192.168.0.1");

    bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */
    /* 不要忘了为bind()做错误检查: */
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

     

     

     


     

     

     

        

               

    给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

    这里写图片描述
    展开全文
  • 在本篇中主要介绍利用C#实现语音通信的基本方法。但是目前只实现了网络上语音传输的基本功能,而且比较粗糙,没有采用什么算法来优化,所以大家千万不要期望过高。我写这篇的目的除了记录自己的经历之外,更希望有...

     现在时下的VOIP软件很多,比较有名的就是Skype,还有其它诸如UUcall、快门等等。它们提供的功能除了网络上的语音通话外,还可以与固定电话、手机等通话。在本篇中主要介绍利用C#实现语音通信的基本方法。但是目前只实现了网络上语音传输的基本功能,而且比较粗糙,没有采用什么算法来优化,所以大家千万不要期望过高。我写这篇的目的除了记录自己的经历之外,更希望有高手能给出改进的意见或算法。

         开发平台:.NET Framework 2.0 ,VS 2005,Windows XP,DirectX SDK(June 2008)下载页面 。
         开发语言:C#。

         测试环境:Windows XP 、.net framework 2.0、普通局域网。
         测试结果:在多台安装了windows XP系统且配置不同的电脑上测试,均能正常运行。可以进行语音对话,但是有明显的杂音,沿时低。

         限于篇幅,在本文中会详细介绍本人认为比较关健的问题,其它部分只做大概介绍,为了便于大家理解,可以先阅读:
         1.DirectX编程:[初级]C# 中利用 DirectSound 录音
         2.C# Socket编程笔记

         在本文中打算按照以下顺序介绍:
         1.项目结果预览与说明
         2.实现方法概要
         3.语言采集
         4.语音传输
         5.语音播放

         项目结果预览与说明

          界面如下:
                        

          说明:界面很简单,只提供了一个选择或输入对方IP的功能,当选择合适局域网内IP之后,单击确定便激活了语音聊天的按钮。如果你想进行语音聊天就可以开始聊天了,聊天端口采用8000。本软件只适用于局域网内用户的聊天,另外因为没有增加用户认证的功能,所以只有在双方都启动了这款软件才能进行通信。如果只想在单机上测试,那只需要选择本机的IP便可。由于囧于技术水平,尝试N次之后,任不知如何才能正确地实现语音效果(如回声消除、降噪等)来保障音质,因此在单机测试会有回声干扰,嚣叫声比较严重,希望高手解囊。

          实现方法概要

          要想实现语音聊天,有几个步骤是必须的(就是我不说,相信你应该也能想得到一些):
          a 语音采集:采集的作用就是从你的麦克风中获取数据,我采用DirectSound类来实现这个技术。参考:C# 中利用 DirectSound 录音
       (b 语音编码:利用语音编码算法对采集到的话音进行压缩编码,进行编码的目的是为了减少网络带宽的压力。)
          c 语音传输:将采集到的声音传输到网络上的其它主机,我采用Socket UDP方式来实现。参考:C# Socket编程笔记
       (d 语音解码:如果所传输的语音进行过压缩编码,则必须对语音进行解码,否则无法得到原始语音数据。)
          e 语音播放:当对方通过网络传输到本机时(,如果需要解码则先执行d),进行实时播放。

          上面红色标记的步骤,可以省略。在本软件中,我并未采用这两个步骤,因为当我采用了这两个步骤后,发现语音时延异常的严重。我采用的编解码算法是G.729,利用的是g729.dll库文件,压缩效果不错,但是时延比较严重,可能是自己哪里没有设置好。如果有朋友使用过该算法,且时延低的,希望不吝赐教。

          接下来,重点介绍语音采集、语音传输、语音播放的实现。

          语音采集
          由于所实现的方法与录音方法一致,因此不会着墨过多,如果你不能很好的理解,请先参考:C# 中利用 DirectSound 录音

          与录音不同的是,录音我们需要建立一个WAVE文件来存储这些采集到的数据,而在语音聊天中,则不需要存储,当采集到一些数据后,就立刻发送出去,因此也不需要开辟很大的空间来存放PCM数据。
          我们先来回顾下采集的基本步骤:
          1. 设置PCM格式,设置相关的参数,如:采样频率、量化位数等。
          2. 建立采集用的设备对象,建立采集用的缓冲区对象。
          3. 设置缓冲区通知,设置通知被触发后的事件。通知是用于当缓冲区的读指针达到某预设位置时触发通知事件,提醒我们可以对某部分的数据进行传送了。
          4. 开始采集声音。
          5. 当通知被触发后,建立一个新的线程来处理数据传送的事件。(建立一个新的线程,就是为了防止采集过程被中断)。


            /// <summary>
            
    /// 设置音频格式,如采样率等
            
    /// </summary>
            
    /// <returns>设置完成后的格式</returns>
            private WaveFormat SetWaveFormat()
            {
                WaveFormat format = new WaveFormat();
                format.FormatTag = WaveFormatTag.Pcm;//设置音频类型
                format.SamplesPerSecond = 11025;//采样率(单位:赫兹)典型值:11025、22050、44100Hz
                format.BitsPerSample = 16;//采样位数
                format.Channels = 1;//声道
                format.BlockAlign = (short)(format.Channels * (format.BitsPerSample / 8));//单位采样点的字节数
                format.AverageBytesPerSecond = format.BlockAlign * format.SamplesPerSecond;

                return format;
                //按照以上采样规格,可知采样1秒钟的字节数为22050*2=44100B 约为 43K
            }

            /// <summary>
            
    /// 创建捕捉设备对象
            
    /// </summary>
            
    /// <returns>如果创建成功返回true</returns>
            private bool CreateCaputerDevice()
            {
                //首先要玫举可用的捕捉设备
                CaptureDevicesCollection capturedev = new CaptureDevicesCollection();
                Guid devguid;
                if (capturedev.Count > 0)
                {
                    devguid = capturedev[0].DriverGuid;
                }
                else
                {
                    System.Windows.Forms.MessageBox.Show("当前没有可用于音频捕捉的设备""系统提示");
                    return false;
                }
                //利用设备GUID来建立一个捕捉设备对象
                capture = new Capture(devguid);
                return true;
            }

            /// <summary>
            
    /// 创建捕捉缓冲区对象
            
    /// </summary>
            private void CreateCaptureBuffer()
            {
                //想要创建一个捕捉缓冲区必须要两个参数:缓冲区信息(描述这个缓冲区中的格式等),缓冲设备。
                WaveFormat mWavFormat = SetWaveFormat();
                CaptureBufferDescription bufferdescription = new CaptureBufferDescription();
                bufferdescription.Format = mWavFormat;//设置缓冲区要捕捉的数据格式
                iNotifySize = mWavFormat.AverageBytesPerSecond / iNotifyNum;//1秒的数据量/设置的通知数得到的每个通知大小小于0.2s的数据量,话音延迟小于200ms为优质话音
                iBufferSize = iNotifyNum * iNotifySize;
                bufferdescription.BufferBytes = iBufferSize;
                bufferdescription.ControlEffects = true;
                bufferdescription.WaveMapped = true;
                capturebuffer = new CaptureBuffer(bufferdescription, capture);//建立设备缓冲区对象

            }

            //设置通知
            private void CreateNotification()
            {
                BufferPositionNotify[] bpn = new BufferPositionNotify[iNotifyNum];//设置缓冲区通知个数
                
    //设置通知事件
                notifyEvent = new AutoResetEvent(false);
                notifyThread = new Thread(RecoData);//通知触发事件
                notifyThread.IsBackground = true;
                notifyThread.Start();
                for (int i = 0; i < iNotifyNum; i++)
                {
                    bpn[i].Offset = iNotifySize + i * iNotifySize - 1;//设置具体每个的位置
                    bpn[i].EventNotifyHandle = notifyEvent.Handle;
                }
                myNotify = new Notify(capturebuffer);
                myNotify.SetNotificationPositions(bpn);

            }

            //线程中的事件
            private void RecoData()
            {
                while (true)
                {
                    // 等待缓冲区的通知消息
                    notifyEvent.WaitOne(Timeout.Infinite, true);
                    // 录制数据
                    RecordCapturedData(Client,epServer);
                }
            }

            //真正转移数据的事件,其实就是把数据传送到网络上去。
            private void RecordCapturedData(Socket Client,EndPoint epServer )
            {
                byte[] capturedata = null;
                int readpos = 0, capturepos = 0, locksize = 0;
                capturebuffer.GetCurrentPosition(out capturepos, out readpos);
                locksize = readpos - iBufferOffset;//这个大小就是我们可以安全读取的大小
                if (locksize == 0)
                {
                    return;
                }
                if (locksize < 0)
                {//因为我们是循环的使用缓冲区,所以有一种情况下为负:当文以载读指针回到第一个通知点,而Ibuffeoffset还在最后一个通知处
                    locksize += iBufferSize;
                }
                capturedata = (byte[])capturebuffer.Read(iBufferOffset, typeof(byte), LockFlag.FromWriteCursor, locksize);
                //capturedata = g729.Encode(capturedata);//语音编码
                try
                {
                    Client.SendTo(capturedata, epServer);//传送语音
                }
                catch
                {
                    throw new Exception();
                }
                iBufferOffset += capturedata.Length;
                iBufferOffset %= iBufferSize;//取模是因为缓冲区是循环的。
            }

          上述代码可以很好的采集到声音数据,几乎与原始声音一致。如果你已经可以实现录音,那么以上对你来说应该并不陌生。

          语音传输
          这部分并不是很难,如果你熟悉socket编程,那么就可以PASS这一部分了,与以往传输不同的只是现在传输的是语音而已。如果你没接触过socket,那可以瞧瞧C# Socket编程笔记

          感觉这部分叫“语音传输”并不是很恰当,因为其实真正用于传输的语句只有一句。除了语音传输之外,我们还需要对网络进行监听,从而能捕获对方发送给自己的语音信息。但是,也不知道叫什么好,就估且这么叫着吧。在这一部分,我主要讲下大致流程。
          1. 建立socket对象,在实例化这个对象的时候有一个参数是设置使用的协议,在本软件中,我采用的是UDP。
          为什么要采用UDP?建立TCP能不能传送语音,答案肯定是能的。在本软件中,我考虑的主要是语音延时问题, 采用TCP在建立连接和维护连接中对时间和系统资源的开销较大,因此会有明显的时延发生,严重影响了实时性。另外,因为UDP是无连接的,这使得采用UDP可以支持日后功能上的扩展(如:组播)。
          2. 绑定本机的IP和端口,因为一个主机可能会有不止一个IP地址,如回发地址:127.0.0.1 和局域网地址:192.168.#.#。为了增加可用性,我这里选择绑定到任何本机可用的IP地址(IPAddress.Any),而端口我们约定默认为8000。
          3. 启动监听线程,来监听网络。我采用异步的方式,以便获得更好的系统响应度。


            private Thread ListenThread;
            private byte[] bytData;

            /// <summary>
            
    /// 监听方法,用于监听远程发送到本机的信息
            
    /// </summary>
            public void Listen()
            {
                ListenThread = new Thread(new ThreadStart(DoListen));
                ListenThread.IsBackground = true;//设置为后台线程,这样当主线程结束后,该线程自动结束
                ListenThread.Start();
            }

            private EndPoint epRemote;

            /// <summary>
            
    /// 监听线程
            
    /// </summary>
            private void DoListen()
            {
                bytData = new byte[intMaxDataSize];
                epRemote = (EndPoint)(new IPEndPoint(IPAddress.Any, 0));

                while (true)
                {
                    if (LocalSocket.Poll(5000, SelectMode.SelectRead))
                    {//每5ms查询一下网络,如果有可读数据就接收
                        LocalSocket.BeginReceiveFrom(bytData, 0, bytData.Length, SocketFlags.None, ref epRemote, new AsyncCallback(ReceiveData), null);
                    }
                }
            }

            /// <summary>
            
    /// 接收数据
            
    /// </summary>
            
    /// <param name="iar"></param>
            private void ReceiveData(IAsyncResult iar)
            {
                int intRecv = 0;
                try
                {
                    intRecv = LocalSocket.EndReceiveFrom(iar, ref epRemote);
                }
                catch
                {
                    throw new Exception();
                }
                if (intRecv > 0)
                {
                    byte[] bytReceivedData = new byte[intRecv];
                    Buffer.BlockCopy(bytData, 0, bytReceivedData, 0, intRecv);
                    voicecapture1.GetVoiceData(intRecv, bytReceivedData);//调用声音模块中的GetVoiceData()从字节数组中获取声音并播放
                      
    //GetVoiceData()会在下一部分中提到
                }
            }


          4. 数据的发送因为只有一句话,所以我直接放在上一部分的语音采集中了。

    Client.SendTo(capturedata, epServer);//传送语音

     

          语音播放
          最麻烦的就是这部分了,而且感觉现在的实现方法仍然需要改进才好。

          当声音传输到本机后,该怎么样才能让这些数据经过音响设备放出声音来呢?因为声音播放是从缓冲区中获取声音数据的因此我们必须先将获取到的数据写入缓冲区,然后再调用相应的方法来播放。看起来似乎不复杂,可是实现起来远没有这么简单。
          我遇到的问题:
          大家可以看下语音采集部分,我是在每次通知后进行语音采集然后就将采集到的语音发送到网络上,如果运行正常的话,这一部分数据实际播放长度远小于1秒。也就是说对方每次接收到的语音长度为毫秒级。而且如果网络质量可以的话,那么连续两次接收到数据的时间间隔也是相当小的。这样就产生问题了,如果我在接收到第一次数据后,将它写入缓冲区,然后调用相应的播放方法,由于语音长度实际很短,因此几乎听不到什么效果,而且可能发生当第一次缓冲区中的数据还没播放完,就已经被第二次的数据覆盖,导致声音混乱。经测试,此种方法无法达到声音实时效果。期间我也曾修改过数据发送部分,希望当语音长度达到某一长度时在发送,可是问题依旧,看样子重要的是在接收端进行相应处理。
          直接缓冲播放的方法不行,那就换~~
         上网搜,可惜的是这方面的资料实在有限,C#的就更少了。参考一些文献,大家提到利用在缓冲区设置两个指针,一个播放指针,一个写指针(写指针用于表示当前从网络上接收到的数据从写指针所指示位置开始往下写,播放指针则表示当前所播放的数据末尾)。当播放指针达到某个位置时就播放某一部分数据,而不影响将被写入的缓冲区部分,这样就可以很好的解决数据覆盖的问题。除此之外,还要将缓冲区设置为循环缓冲区,也就是头尾相接,当到达尾部时,自己从部开始,此时将覆盖头部数据。
          看了这些,你是不是感觉很眼熟?是不是和语音采集很类似?是的,我们在捕捉缓冲区中就是这样设置的,我们利用通知来设置触发事件。不同的是我们接收语音用的缓冲区并不是捕捉缓冲区,MS为捕捉单独设置了一个捕捉缓冲区。我们利用的是另一个缓冲区,辅助缓冲区(SecondaryBuffer)。后来发现该缓冲区也有类似的通知,这意味什么?我当时很兴奋,可是~~相当郁闷的是,我不管怎么设置通知,编译时都会报错,到外询求答案,均无果。在 MS 相关网站上咨询后,有一位叫jwatte的答案,让我又高兴又失望:

          原话如下:

          Notify is broken in DirectSound, has been for a long time, and probably will never be fixed.

          The only way to know when you need to play the next piece of data is to check the play pointer each time through your main loop, and then lock the buffer and fill in whatever part has been played out.

          Also, DirectSound is now in "maintenance" mode, and won't be further developed by Microsoft. Instead, for game applications, they recommend you use XAudio2 to play sound.

          简单意思就是:Notify出问题已经很长时间了,而且MS可能永远都不会去修复这个问题。而且他也为播放声音提供了些建议,这些建议与上面所讲的基本一致。
          至于这个答案是否正确,因为无从考证,就不再讨论了。如果哪位高手曾经实现过,希望赐教。
          既然目前无法正常使用,就只能来手动写了。这个方法名就是:GetVoiceData()。

          思路如下:
          ·利用MemoryStream来代表这个接收缓冲区。
          ·设置两个表示指针位置的字段:
             private int intPosWrite = 0;//内存流中写指针位移
             private int intPosPlay = 0;//内存流中播放指针位移
          ·当接收到数据后,则移动写指针,移动的长度为接收到的数据长度。
          ·利用一个字段表示通知大小:private int intNotifySize = 5000;
          ·当写指针的位置达到通知大小,则执行播放操作,然后移动播放指针到刚才的通知的位置。
          ·如果当前写指针的位移与将要写入到缓冲区的数据大小相加后超过缓冲容量的,则进行摩尔运算,实现循环的效果。


            private int intPosWrite = 0;//内存流中写指针位移
            private int intPosPlay = 0;//内存流中播放指针位移
            private int intNotifySize = 5000;//设置通知大小

            /// <summary>
            
    /// 从字节数组中获取音频数据,并进行播放
            
    /// </summary>
            
    /// <param name="intRecv">字节数组长度</param>
            
    /// <param name="bytRecv">包含音频数据的字节数组</param>
            public void GetVoiceData(int intRecv, byte[] bytRecv)
            {
                //intPosWrite指示最新的数据写好后的末尾。intPosPlay指示本次播放开始的位置。
                if (intPosWrite + intRecv <= memstream.Capacity)
                {//如果当前写指针所在的位移+将要写入到缓冲区的长度小于缓冲区总大小
                    if ((intPosWrite - intPosPlay >= 0 && intPosWrite - intPosPlay < intNotifySize) || (intPosWrite - intPosPlay < 0 && intPosWrite - intPosPlay + memstream.Capacity < intNotifySize))
                    {
                        memstream.Write(bytRecv, 0, intRecv);
                        intPosWrite += intRecv;

                    }
                    else if (intPosWrite - intPosPlay >= 0)
                    {//先存储一定量的数据,当达到一定数据量时就播放声音。
                        buffDiscript.BufferBytes = intPosWrite - intPosPlay;//缓冲区大小为播放指针到写指针之间的距离。
                        SecondaryBuffer sec = new SecondaryBuffer(buffDiscript, PlayDev);//建立一个合适的缓冲区用于播放这段数据。
                        memstream.Position = intPosPlay;//先将memstream的指针定位到这一次播放开始的位置
                        sec.Write(0, memstream, intPosWrite - intPosPlay, LockFlag.FromWriteCursor);
                        sec.Play(0, BufferPlayFlags.Default);
                        memstream.Position = intPosWrite;//写完后重新将memstream的指针定位到将要写下去的位置。
                        intPosPlay = intPosWrite;
                    }
                    else if (intPosWrite - intPosPlay < 0)
                    {
                        buffDiscript.BufferBytes = intPosWrite - intPosPlay + memstream.Capacity;//缓冲区大小为播放指针到写指针之间的距离。
                        SecondaryBuffer sec = new SecondaryBuffer(buffDiscript, PlayDev);//建立一个合适的缓冲区用于播放这段数据。
                        memstream.Position = intPosPlay;
                        sec.Write(0, memstream, memstream.Capacity - intPosPlay, LockFlag.FromWriteCursor);
                        memstream.Position = 0;
                        sec.Write(memstream.Capacity - intPosPlay, memstream, intPosWrite, LockFlag.FromWriteCursor);
                        sec.Play(0, BufferPlayFlags.Default);
                        memstream.Position = intPosWrite;
                        intPosPlay = intPosWrite;
                    }
                }
                else
                {//当数据将要大于memstream可容纳的大小时
                    int irest = memstream.Capacity - intPosWrite;//memstream中剩下的可容纳的字节数。
                    memstream.Write(bytRecv, 0, irest);//先写完这个内存流。
                    memstream.Position = 0;//然后让新的数据从memstream的0位置开始记录
                    memstream.Write(bytRecv, irest, intRecv - irest);//覆盖旧的数据
                    intPosWrite = intRecv - irest;//更新写指针位置。写指针指示下一个开始写入的位置而不是上一次结束的位置,因此不用减一
                }
            }

          
           这样,基本上就可以实现语音聊天了。可是这样的效果还只能是初步的,而且由于回声的原因,相当影响音质,还可能产生嚣叫,为了解决这个问题,我本打算采用MS提供的AEC算法,可是由于不知道如何实现,一直无法得到效果,因此这也是比较遗憾的地方。

    展开全文
  • 首先介绍相关知识。 UDP协议 UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不...
  • 只是类似 局域网相互发现并添加对放并显示出来, 可以群发信息 是真正的群发 只需要发送一次所有人就可以全部收到不用for遍历所有ip挨个发送真正的发送一次。 也可以单个发送,可以单个 多个 全部发送信息并且加抖动...
  • 应用场景如下,本人用51单片机联合ESP8266制作了一个温度湿度检测装置,通过espWiFi芯片把测得数据以广播形式发送在了网络中,在不指定目的主机的情况下,需要对于发送出来的数据进行接收并显示,请问如何实现?
  • 局域网QQ(C#版)

    2006-11-23 16:37:00
    <!--<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"xmlns:dc="http://purl.org/dc/elements/1.1/"xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"><rdf:Desc
  • 偶公司是搞通信的,所以客服人员比较多(啥,不知道啥是客服,那你总知道10086吧,没错就是接电话的). 当然了,也有相关的坐席接电软件,不是买的哦,今年上半年啥事没有干就做了这个坐席软件. 其他不多说了,坐席人员反映...
  • 服务器端 需要的引用包括: using System.Net; using System.Net.Sockets; using System.Threading; 首先要创建Socket,将其绑定到本机的ip地址,给它一个端口号。然后开始监听 什么是ip地址和端口号?...
  • Android下实现局域网设备发现与通信 在使用Android开发智能设备时,一般会分为用于遥控与管理的Host端,和用于执行个性功能的Slave端,二者可以借助网络或蓝牙等途径实现通信局域网设备发现 如果是借助网络...
  •  TCP,UDP是运输层两种重要的通信协议,向上层的应用层提供通信服务,同时也向下屏蔽了网络通信的细节。  TCP较为复杂,是面向有连接的、可靠的运输层通信协议,有确认机制;这一定程度上限制了该协议对于数据...
  • 我的问题是这样的: 用C #编写上位机(服务端)与多个模块(客户端)在不同网段(多个路由连接 )进行通信,并要求服务端设置客户端网关和地址。怎么使用Socket编程?希望能请教个位大侠,谢谢!
  • 学习remoting的一个测试,由服务端、客户端构成的一个简易局域网聊天室,均为winform,对C#初学者应该有点用。客户端使用udp查找服务端,通过remoting与服务端通信,服务端可对在线用户广播信息,客户端信息通过...
  • 局域网QQ(C#版)

    2019-07-24 18:51:11
    本通讯程序没有服务端和客户端之分,局域网的计算机运行同一程序即可通信。由于水平有限,目前版本还很菜,只可以实现基本的聊天功能和显示在线用户功能。准备添加传送文件、聊天日志、可选参数等功能。 ...
  • using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.Net; using System.Net.Sockets; ...using
  • C#实现 UDP简单广播

    2016-11-21 13:54:02
    [csharp] view plain copy print? 代码 Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> class Program { static bool co
1 2 3 4 5 ... 20
收藏数 879
精华内容 351