1、 Socket 通信实验: 使用 socket 进行通信程序设计 姓名:于广溪 学号: 11300720070 系别:通信科学与工程 日期: 2013年 11月 7 日 目录: 一、 引言 二、 实验基本原理 三、 主要数据结构 四、 主要程序流程 五、 安装和使用说明 六、 附录 1:程序源代码 七、 附录 2: 测试效果 及 程序代码 获取 地址 八、 附录 3:参考资料 正文 : 一、 引言 1. 实验目的: 本次实验的目的主要有一下三条: a. 理解 Socket(管道)通信的基本原理 b. 掌握通过 socket 编程实现 C/S 程序的基本方法 c. 掌握利用 Windows So
2、cket 函数库在 Win32 平台下编制通信程序的方法 2. 实验内容 : 编写一个界面友好的 网络通信应用程序 ,包括服务器端和客户端两个程序。能实现以下功能:可以支持多人进行文字聊天。 为了达到本次实验的目的并完成实验内容,选择了用 C语言编写服务器端程序而采用 VB.NET 编写客户端的方法。由于 本次实验基于Windows操作系统,所以编写界面友好型的程序选用 VB.NET 是十分适合的;而服务器端由于对性能的要求比较高并且不呈现给用户所以采用了 C 语言编写为控制台程序。出于可移植性的考虑,文章最后亦给出了客户端 C 语言的代码版本,以便在需要时参考使用。 本次实验达到了多人进行文
3、字聊天的功能,即实现了一个网络聊天室。如果服务器端 IP地址是公网可方便进行网络间通信聊天。但如果服务器端 IP 是内网地址则程序实现局域网内聊天,对于 IP 穿透问题本次实验不作考虑。 二、 实验基本原理 1. 源代码编写语言 a. 本次实验服务器端源代码使用 C 语言编写, C语言的主要优点是效率高 ,能够提供服务器端需要的执行能力,支持较多用户同时在线,完成 Socket 的 建立连接,中转客户间的数据进行通信。 b. 本次实验客户端源代码使用 VB.NET 编写, VB.NET是基于微软 .NET Framework 之上的面向对象的中间解释性语言,可以看作是 Visual Basic
4、 在 .Net Framework 平台上的升级版本,需要在 .Net Framework平台上才能执行,增强了对面向对象的支持。使用 VB.NET 可以直接画出窗体然后依次编写代码因此在性能 负担很小而对界面有较多要求的客户端采用了这种编程语言。 2. Socket 函数和类 1).Winsocket 套接字 API 的实现 : C 语言中的通信主要使用 Winsocket的系列 函数 ,这一系列函数在库文件 winsock.h 中进行了定义。由于对于windows这样可以加载多个程序进入内存的复杂操作系统而言重复加载同样的共享代码没有必要,所以自 windows95 起使用了成为动态链接库
5、来提供众多可以被公用的应用程序接口( API)。一般动态链接库以“ .lib”后缀命名。在本次实验的服务器 C 语言源代码中起始部分就为 winsock.h指明了相应的动态链接库文件“ ws2_32.lib” .。关于应用程序,套接字 API, TCP/IP 函数和 I/O 函数的关系可以参见下图所示: 2).Winsocket提供并在本次实验中用到的主要函数: A. WSAStartup函数:在使用 windows套接字之前必须调用此函数来寻找合适的链接库并绑定它。其参数有两个,第一个指定使用的套接字版本,第二个有操作系统返回该套接字实际使用的版本。第一个参数是一个整数,用十六进制数表示。第
6、二个参数指向一个 WSADATA结构,操作系统将实际版本信息填入其中。 B. WSACleanup函数:关闭套接字使用,释放所有数据结构和套接字绑定。 C. Socket 函数:创建一个新的套接字用于网络通信。函数返回一个描述符表示这个套接字。参数可以指定使用的协议族(如 PF_INET 代表 TCP/IP)和协议、服务类型(如字节流或数据报)。对于使用 internet协议族的套接字来说,协议、服务类型实际就睡 tcp 还是 udp。 D. connect函数:创建一个套接字后,客户端就可以用connect函数来建立连接了。其参数包括远端机器的 IP 地址和端口号。 E. send函数: 连
7、接建立之后可以使用该函数来进行数据发送。其包括四个参数,套接字描述符、发送数据源地址、数据长度、控制传输的位标识。 Send通常会将要发送的数据送入操作系统缓冲区以便程序可以继续执行,只有当系统缓冲区 满时才会调用阻塞。 F. recv函数: 连接建立之后可以使用该函数来进行数据接收。 其包括四个参数,套接字描述符、接收数据目的地址、缓冲区长度、控制接收的标志位。 Recv 获取到达套接字的数据后把它们复制到用户缓冲区。如果没有数据到达,调用就阻塞知道数据到达。如果到达数据超过缓冲区容量,则只提取有限的数据并返回获得的数据大小。 G. closesocket 函数:一旦套接字使用完毕, 应该
8、调用closesocket 来释放它。 H bind函数:套接字创立时没有端点地址,应用程序使用 bind 函数来指定端点地址。对于 TCP/IP 协议来说,端点地址用sockaddr_in 结构,它包括一个 IP 地址和协议端口号。 I listen 函数:套接字创立时既非主动(客户端,主动进行连接)亦非被动(服务器端,被动等待连接) 。 Listen 函数在服务器端被调用时将套接字指定为被动状态,等待连接的建立。一般在服务器端会存在一个无限循环,他接受并处理传入的连接,然后返回并接受处理下一个连接。 即使处理一个连接只要几毫秒,但还是有可能出现系统忙于处理一个连 接的时候另一个连接请求到达
9、了的情况。为了保证没有连接请求被忽略,服务器端程序必须传递两个参数给 listen参数,告诉操作系统对某个套接字上的连接进行排队。两个参数其一指定要置于被动态 的套接字,另一个指定用于该套接字的队列大小。本实验队列大小定义为 5。 J. accept 函数:对于 TCP 套接字,服务器程序调用socket 创建一个套接字,调用 bind 绑定地址后会使用 listen 将其置为 被动模式,然后调用 accept 获取一个传入请求。 Accept 的一个参数指定套接字并从这个套接字上接受连接及为新的连接创建新的套接字。 Accept创建新的连接的套接字后返回新的套接字描述符 ,服务器用这个新的套
10、接字与客户进行通信,原来的套接字用来接受其他的连接。 K. htos函数:由于本地字节顺序与网络字节顺序不同,故在指定端点地址时需要进行字节顺序转换。由于实际使用中希望尽量方便,本次实验服务器端指定了端口为 32007 而只需用户填入会改变的 IP地址。但是这个端口是本地存储的所以使用了 htos 进行转换。 3) socket 类 ( VB.NET 中使用) 区别于 C语言中的 Winsocket 系列函数,在 VB 向 VB.NET的改变进程中,自 2005 版之后已经取消了这类函数(控件)。虽然 VB 6.0 写出的程序仍然可以在 win7上运行,但是考虑到发展趋势本次试验放弃了 VB
11、中的这类控件而使用最新版 VB.NET中的 socket类。 Socket 类的继承层次结构为 System.Net.Sockets.Socket,命名空间: System.Net.Sockets,其程序集为 System(system.dll动态链接库中 )。 本程序中使用了 socket 类中的一个构造函数:Socket(AddressFamily, SocketType, ProtocolType) 。它的三个参数指定 地址族、套接字类型和协议 。该函数用来 初始化 Socket 类的新实例 。本实验客户端调用该函数时指定参数为( AddressFamily.InterNetwork,
12、SocketType.Stream, ProtocolType.Tcp)即指定 IPV4 协议族,数据流, TCP 协议。 使用 Connect(EndPoint)方法进行连接, Endpoint类包含了IP地址和端口地址。 使用 Receive(Byte()方法从套接字接受数据放入缓存区。 使用 Send(Byte()方法将数据发送到套接字上。 3. 并发与异步 IO 中断 由于实验要求允许多人同时聊天,即实现聊天室的功能,所以对于服务器端来说并发是必须的。服务器应该能同时完成这样的三个功能:等待新的连接接入并为之创建新的套接字,从已经创建的套接 字上接收数据,从已经创建的套接字上发送数据。
13、而对于客户端来说也应该能同时收发数据,所以在 C/S 两端都应该能实现并发。 然而,对于服务器端和客户端来说并发的要求又有所不同。服务器端由于支撑着多个用户进行聊天所以如果采用多线程或者多进程的形式势必会创建至少 2 倍 于 客户端的线程 /进程数。这对服务器能支撑的在线用户数做出了极大限制。 早期的服务器端也确实是使用这种并发方式的。 这种并发实现带来的死锁问题也是一个解决的难点。 最近几年,服务器的并发方式有所改进,大多使用了单线程的方式实现。这样操作系统用于调度线程 /进程的资源就可以节省出来, 死锁的问题也自然消失了,使得 服务器能 轻松 支撑百万人级别的服务。 尽管相较于以前的传统并
14、发实现方式,单 线程有着如此优势,但是这种方式不符合人的思维方式,编程相比较为复杂。对于负载十分轻的客户端来说,使用 IO 异步中断也确实没有必要。 因此 在 本次实验中,服务器端实现并发的方式选用了单线程 IO异步中断,客户端实现并发采用多线程。这样能充分发挥各自并发实现方式的优点。 1) 异步 I/O简介 当 socket 函数 recv, send 被调用,或者程序正在进行accept 时程序会阻塞在这里而不能继续运行。因此才无法实现并发而需要多线程 /进程。但是 用异步 I/O 的方法就能在一个线程内解决这个并发问题。首先,程序维持一个 Socket 的监 视列表(用 FD_SET 函
15、数将一个 socket 描述符加入到表中)然后使用 select 函数对表格进行检测,当发现在表格中的某个 socket 上有数据到达时就调用自定义的处理函数进行处理。 2) 异步 I/O的实现方法 异步 I/O 的实现主要使用到FD_ZERO,FD_SET,FD_ISSET,FD_CLR,select 共 5个函数。 FD_ZERO用于把列表初始化,即清零。 FD_SET 把一个 socket 加入到列表中 FD_ISSET 用来判断调用 FD_ISSET 时指定的 socket(由调用时的第一个参数指定)在不在列表中,即判断此 socket 是否可读写。 FD_CLR 用来将指定的 soc
16、ket(由调用时的第一个参数指定)清除出列表。 Select 用来进行检测,其定义如下: int select( _In_ int nfds, Inout_ fd_set *readfds, _Inout_ fd_set *writefds, _Inout_ fd_set *exceptfds, _In_ const struct timeval *timeout ); 实验中调用使用的参数为 : FD_SETSIZE, U_short sin_port; Struct in_addr sin_addr; Char sin_zero8; ); 3. 套接字表的数据结构: 此表用来存储已经创建的
17、 socket 描述符,以实现异步I/O 使用,其结构定义为: typedef struct fd_set u_int fd_count; /* how many are SET? */ SOCKET fd_arrayFD_SETSIZE; /* an array of SOCKETs */ fd_set; 4. VB.NET 中的网络端点地址结构: IPEndPoint类的定义如下 Public Sub New ( _ address As IPAddress, _ port As Integer _ ) IPAddress.Parse 方法定义如下: Public Shared Funct
18、ion Parse ( _ ipString As String _ ) As IPAddress 四、 主要程序流程 1. 服务器端的程序主要流程说明: 1) 总体流程图说明 2) 流程文字说明 程序开始时先调用 cresockbyport 建立了一个主套接字 msock。这 时 主套接字已经处于 listen 状态。主套接字的作用是接受新的连接请求,然后为新的连接建立新的套接字进行数据通信。主套接字本身并不与客户机进行消息的直接通信。消息的通信是从套接字,即刚刚提到的新建立的套接字上实现的功能。服务器端 为 每个 客户连接请求建立一个从套接字进行消息通信。然后系统初始化了套接字表。套接字表
19、有两个,其中一个套接字表( afds)上面记录了程序创建的所有当前仍然有效(未断开)的套接字;另一个套接字表( rfds)用来传递给 select函数进行检测。这里使用两个套接字表的原因是因为 select 进行检测时,会把没有数据到达的套接字从表中删除而只留下有数据到达的套接字。即套接字表( afds)是用来存储所以有效套接字的而套接字表( rfds)是用来让 select进行检测的。所以程序中看到初始化的是套接字表afds,然后把主套接字 msock 加入到了 afds 中。 程序之后进入一个无限循环。循环首先把 afds 表中的套接字复制到 rfds中。这样就可以维持一张有所有有效套接字
20、的表和一张用于 select检测的套接字表了。复制好以后程序调用 select 来检测 rfds 表中的套接字哪个上面有数据到达了,如果有数据到达则对应的套接字留在rfds表中,没有数据到达的套接字则被从 rfds 表中删除。当然,无论有无数据到达, afds表中都有套接字的记录。 完成检测后进入一个 if判断,检测主套接字是否还在rfds表中,即 rfds主套接字上是否有数据到达(是否有人向服 务器发送了建立连接的请求) 。如果有则为新的连接请求建立一个套接字并把该套接字加入到 afds表中( afds 中记录所有有效的套接字)然后执行下面的语句;如果没有则直接执行下面的语句。 此处逐个检查
21、 rfds表中的套接字,如果到达的数据是正数(即到达的数据不是连接断开的说明)则调用readsend 函数对该套接字上到达的数据进行处理。如果到达的数据非正数(即此次到达的数据是连接断开的说明)那么就把该套接字从 afds表中清除,因为此套接字已经断开,是无效的了。 Readsend 函数首先从参数中指定的有数据到达的套接字上 读取数据。因为已经确定此时有数据到达该套接字所以 recv 不会阻塞。然后调用 send 函数, 把接受到的信息发送给 afds表(调用 readsend函数时由参数传入)中所有有效的套接字,即发送给所有人。 2. 客户端的程序主要流程说明: 1) 总体流程图说明 2)
22、 流程文字说明 客户端的程序编写采用面向对象的 VB.NET 语言。其流程与面向过程的 C语言完全不同。 VB.NET 中首先用窗体画出了两个文本框和两个富文本框以及三个按钮。两个文本框分别用来接收客户键入的服务器端 IP 地址和客户的用户名(发送的信息都在结尾自动加上用户名以方便查看)。两个富文本框中一个是只读的,用于显示聊天记录,另一个可写,是待发送文本键入区。三个按钮中依次为登陆, 离开 ,发送。对应实现的功能如名字所示。在窗体布局时为每个控件指定了tabindex。服务器 IP 键入文本框的 tabindex 值为 0,即程序开始时默认选中此文本框方便用户进行键入。用户名键入文本框 t
23、abindex 值为 1。所以当用户键入 IP后按下 tab 键即可进行用户名的输入,然后 tabindex 值按登陆按钮,待发送文本键入框,发送按钮,离开按钮,聊天记录按钮的顺序依次增加。这样用户只需要顺序按下 tab 键即可方便进行聊天,降低了鼠标的使用需要,提高了用户体验。在对象的行为代码中也有这样的考虑,这将在下面进行说明。 登陆按钮单击事件的代码: 创建套接字并用用户键入的IP地址登陆服务器。如果连接失败则弹出一个消息对话框告知用户原因并退出。如果成功则弹出对话框提示用户连接服务器成功,可以开始聊天了。成功连接并告知用户后程序把IP键入框,用户名框和登陆按钮的有效性置为 0,使得用户
24、不可对这三个对象进行操作除非关闭程序后再次打开程序。随后程序把待发送文本键入的文本框设成当前焦点,方便用户键入聊天内容。然后程序开启一个新的线程,在这个新的线程中调用 socket类的实体 Sclient的方法 receive来接收数据,并把接受到的数据填入聊天记录的文本框显示给用 户查看。 离开按钮单击事件的代码:关闭套接字结束连接,然后退出程序 发送按钮单击事件的代码:调用 socket 类的实体Sclient的方法 send 把待发送文本键入框内的消息以及用户名发送出去。然后把待发送文本键入框清零,使得文本框得到 焦点方便用户进行下一次输入进行聊天。 五、 安装和使用说明 本次实验生成的
25、服务器及客户端程序都不需要安装即可直接使用。但是需要说明的是由于软件涉及线程的创建所以有可能被安全软件误认为病毒文件。经测试, 360 安全卫士会对 客户端程序 进行 木马 报毒 。使用时需要将客户端程序设置成信任文件不受监视才可正常使用。服务器程序由于申请网络资源 , 会出现 windows 防火墙提示,默认选择允许访问网络即可。 1. 服务器使用说明: 服务器程序应该先于客户端程序打开,否则客户端程序会提示错误,连接不上服务器。双击 chatserver.exe文件即可打开服务器程序。服务器打开后会出现命令控制台窗口,此时窗口中没有任何输出则表明运转正常。打开服务器程序后无需任何操作即可自
26、动完成所有必要功能。当有客户端程序连接进服务器时,服务器会打印“ A new connection is creadted!”。当用户退出客户端程序时服务器端会打印“ echo receive error 数字”表明有用户端客户程序关闭了连接。关闭服务器程序只需点击窗口右上角的关闭按钮即可。 如果 希望服务器程序把消息记录在一个文件里可以执行下面的操作: a. 在 windows 操作系统下按 windows 徽标键 +R 打开“运行”提示 b. 在“运行”提示中键入 cmd打开控制台 c. 键入“ cd chatserver 所在目录”(没有引号)进入程序所在目录 d. 键入“ chatse
27、rver record.txt” (record是文件名,可以自行命名 ) e. 如需关闭则按下 Control+C 即可。此时记录就在record.txt中了 2. 客户端使用说明: 客户端程序应该后于服务器程序打开,否则客户端显示无法连接到服务器。使用前 应向 服务器端确认服务器端的 IP 地址(如果服务器不清楚可以在命令提示符中键入 ipconfig 命令查询)。双击打开 chatclient.exe 文件之后会打开一个窗口,上面有文本键入框和按钮。 a. 在对应区域填入服务器 IP 地址,自己要使用的用户名(按 Tab键即可切换至下面的用户名文本框,填好后再按 Tab 就选中了登陆按钮
28、) b. 按下“登录”按钮,此时连接成功后会提示已经成功连接,可以进行聊天了。 c. 在“请输入”后的文本框键入要发送的内容,可以用回车换行。写好后按下发送按钮即可发送。 d. 从聊天记录 区域 可以看到自登陆以来聊天室中所有聊天记录,可滚动翻页。 e. 要退出程序可点击“离开”按钮即可 。 六、 附录 1:程序源代码 1. 服务器端程序源代码: 1) Passsock.cpp #include #include #include #define QEN 5 /主套接字允许 的队列大小,非允许的客户数 void errexit(const char *, .); SOCKET cresockb
29、yport(unsigned short port) struct sockaddr_in sin; /* an Internet endpoint address */ SOCKET s; /* socket descriptor */ /*initial sin*/ memset( sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port=htons(port); /* Allocate a socket */ s = socket(PF_INET, SOCK_STREAM, 0); if (s = I
30、NVALID_SOCKET) errexit(“cant create socket: %dn“, GetLastError(); /* Bind the socket */ if (bind(s, (struct sockaddr *) if (listen(s, QEN) = SOCKET_ERROR) errexit(“cant listen on %s port: %dn“, (char)port,GetLastError(); return s; 2) Chatserver.cpp #include #include #pragma comment(lib,“ws2_32.lib“)
31、 #include #include #include #include #include #define BUFSIZE 4096 #define WSVERS MAKEWORD(2, 0) #define DEFAULPORT 32007 void errexit(const char *, .); SOCKET cresockbyport(unsigned short); int readsend(SOCKET,SOCKET,fd_set *); int main(int argc, char *argv) struct sockaddr_in newconnadd; /* the ad
32、dress of a client */ int newconnaddlen; /* length of clients address*/ unsigned short defaultport=DEFAULPORT; /define default port for this chat service,user can change it when run the server exe. SOCKET msock; /main socket for accepting new connection fd_set afds,rfds; /all socket will be added to
33、the afds while rfds is used for selecting whether a socket is ready for reading WSADATA wsadata; unsigned int resefdcount; /used for counting.this variety used to designate with which socket to recv and send if (WSAStartup(WSVERS, msock=cresockbyport(defaultport); FD_ZERO( FD_SET(msock, while (1) me
34、mcpy( if (select(FD_SETSIZE, if (FD_ISSET(msock, newconnaddlen = sizeof(newconnadd); ssock = accept(msock, (struct sockaddr *) if (ssock = INVALID_SOCKET) errexit(“accept: error %dn“,GetLastError(); else printf(“A new connection is created!n“); FD_SET(ssock, for (resefdcount=0; resefdcount1) for(i=0;i(*afdsrs).fd_count;i+) fdrs=(*afdsrs).fd_arrayi; if(fdrs!=msockrs) if (send(fdrs, buf, cc, 0) = SOCKET_ERROR) errexit(“echo send error %dn“, GetLastError(); return cc; 2. 客户端程序源代码( VB.NET):