赞
踩
TCP/UDP程序开发
开发TCP/UDP协议应用程序,掌握网络应用程序的工作原理。通过该实验,深入理解UDP和TCP协议的异同点,了解网络协议的工作过程,学会网络通信编程的基本方法,能够编制网络应用程序。
(1)了解和掌握“基于UDP-面向无连接的应用程序/基于TCP-面向连接的应用程序”的运行机制和编程方法;
(2)编写一个网络通信应用程序:聊天程序;
(3)使用任意网络编程语言(Java、C、VB、Delphi、Python等)编写基于TCP或UDP协议的网络应用程序。
(4)总结实验过程:方案、编程、调试、结果、分析、结论。
Win Sock编程是一种网络编程接口,实际上是作为TCP/IP协议的一种封装。可以通过调用WinSock的接口函数来调用TCP/IP的各种功能。
WinSock 编程简单流程:WinSock编程分为服务器端和客户端两部分。
2.1 使用WSAStartup()函数检查系统协议栈安装情况;
2.2 使用socket()函数创建服务器端通信套接字;
2.3 使用bind()函数将创建的套接字与服务器地址绑定;
2.4 使用listen()函数使服务器套接字做好接收连接请求准备;
2.5 使用accept()接收来自客户端由connect()函数发出的连接请求;
2.6 根据连接请求建立连接后,使用send()函数发送数据,或者使用recv()函数接收数据;
2.7 使用closesocket()函数关闭套接字;
2.8 最后调用WSACleanup()函数结束Winsock Sockets API。
3.1 使用WSAStartup()函数检查系统协议栈安装情况;
3.2 使用socket()函数创建客户端套接字;
3.3 使用connect()函数发出也服务器建立连接的请求(调用前可以不用bind()端口号,由系统自动完成);
3.4 连接建立后使用send()函数发送数据,或使用recv()函数接收数据;
3.5 使用closesocet()函数关闭套接字;
3.6 最后调用WSACleanup()函数,结束Winsock Sockets API。
在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
socket是利用三元组解决网络通信的一个中间件工具,就目前而言,几乎所有的应用程序都是采用socket,如UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰)。
Socket通信的数据传输方式,常用的有两种:
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
socket函数的三个参数分别为:
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当调用socket创建一个socket时,返回的socket描述字它存在于协议簇(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。当我们调用socket创建一个socket时,返回的socket描述字它存在于协议簇(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
函数的三个参数分别为:
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
read()函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write()函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置error变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能:(1)write的返回值大于0,表示写了部分或者是 全部的数据。(2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
3.1 连接建立:服务器调用socket()、 bind()、 listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
3.2 数据传输:建立连接后, TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
3.3 关闭连接:如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
#define _CRT_SECURE_NO_WARNINGS 1 #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <stdio.h> #include<WinSock2.h> #pragma comment(lib,"ws2_32.lib") #include <windows.h> SOCKADDR_IN cAddr = { 0 }; int len = sizeof cAddr; SOCKET clientSocket[1024]; void func(int index) { //通信 char buff[1024]; int r; while (1) { r = recv(clientSocket[index], buff, 1023, NULL); if (r > 0) { buff[r] = 0; printf("%s发来的数据:%s\n", inet_ntoa(cAddr.sin_addr), buff); } } } int main() { //确定协议版本 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion)!=2) { printf("确定协议版本失败!\n"); return -1; } //创建socket SOCKET serverSocket=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (SOCKET_ERROR == serverSocket) { printf("创建socket失败:%d\n", GetLastError()); //清理协议版本信息 WSACleanup(); return -1; } printf("创建socket成功!\n"); //创建服务器协议地址簇 SOCKADDR_IN addr = { 0 }; addr.sin_family = AF_INET;//协议地址簇 addr.sin_addr.S_un.S_addr = inet_addr("192.168.217.1"); addr.sin_port = htons(9853);//10000左右 小端转大端 //绑定 int r = bind(serverSocket, (struct sockaddr*)&addr, sizeof addr); if(-1==r) { printf("绑定失败:%d\n", GetLastError()); //关闭socket closesocket(serverSocket); //清理协议版本信息 WSACleanup(); return -1; } printf("绑定成功!\n"); //监听 r = listen(serverSocket, 10); if (r == -1) { printf("监听失败:%d\n", GetLastError()); //关闭socket closesocket(serverSocket); //清理协议版本信息 WSACleanup(); return -1; } printf("监听成功!\n"); //等待客户端连接 for (int i = 0; i < 1024; i++) { clientSocket[i] = accept(serverSocket, (sockaddr*)&cAddr, &len); if (SOCKET_ERROR == clientSocket[i]) { printf("客户端连接失败:%d\n", GetLastError()); closesocket(serverSocket); WSACleanup(); return -1; } printf("客户端连接成功!\n"); CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)func, (LPVOID)i, NULL, NULL); } //关闭socket closesocket(serverSocket); //清理版本协议信息 WSACleanup(); while (1);//停顿 return 0; }
#define _CRT_SECURE_NO_WARNINGS 1 #define _CRT_SECURE_NO_WARNINGS 1 #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <stdio.h> #include<WinSock2.h> #pragma comment(lib,"ws2_32.lib") #include <windows.h> int main() { //确定协议版本 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("确定协议版本失败!\n"); return -1; } //创建socket SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (SOCKET_ERROR == clientSocket) { printf("创建socket失败:%d\n", GetLastError()); //清理协议版本信息 WSACleanup(); return -1; } printf("创建socket成功!\n"); //获取服务器协议地址簇 SOCKADDR_IN addr = { 0 }; addr.sin_family = AF_INET;//协议地址簇 addr.sin_addr.S_un.S_addr = inet_addr("192.168.217.1"); addr.sin_port = htons(9853);//10000左右 小端转大端 //连接服务器 int r = connect(clientSocket, (sockaddr*)&addr, sizeof addr); if (-1 == r) { printf("连接服务器失败:%d\n", GetLastError()); closesocket(clientSocket); WSACleanup(); return -1; } printf("连接服务器成功!\n"); //通信 char buff[1024]; while (1) { scanf("%s", buff);//接受用户输入 send(clientSocket, buff, strlen(buff), NULL);//发送给服务器 } //关闭socket closesocket(clientSocket); //清理版本协议信息 WSACleanup(); while (1);//停顿 return 0; }
(1)无法让多个客户端同时发送信息给服务器;
(2)出现’inet_addr’: Use inet_pton() or InetPton() instead or define _WINSOCK_DEPREC报错;
(3)出现’inet_ntoa’: Use inet_ntop() or InetNtop() instead or define _WINSOCK_DEPREC报错;
(1)使用CreateThread函数,CreateThread是一种微软在Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。
(2)该问题出现的原因是在VS2013以后的版本中,增加了inet_pton()、InetPton()之类的新函数,用于IP地址在“点分十进制”和“二进制整数”之间转换,并且能够处理ipv4和ipv6。而inet_addr是老函数,高版本VS在编译时默认使用了新函数,所以会报该错误。因此,只需对VS进行设置让其忽略报错。
(3)该问题出现的原因是socket2已经丢弃inet_ntoa()、inet_addr()这些老版本函数,当在Socket2上调用这些老版本(Socket1版本)函数时,warning变成error了。因此,只需在代码开头加个 #pragma warning(disable:4996) ,用于屏蔽warning,提高兼容性。
调试时我们为了验证其稳定性,及多客户端在同一时间发消息时是否会造成一些忙碌问题我们打开并建立五个客户端端口:
5.3.1 服务端总视图
5.3.2 第一个客户端创建
5.3.3 第二个客户端创建
5.3.4 第三个客户端创建
5.3.5 第四个客户端创建
5.3.6 第五个客户端创建
上述就是同时打开了5个客户端窗口,并通过服务器端窗口可以看出5个客户端全部成功连接。
下面进行测试,看服务端是否可以接收来自每个不同的客户端发来的信息。
5.3.7 第一个客户端发送111
5.3.8 第二个客户端发送222
5.3.9 第三个客户端发送333
5.3.10 第四个客户端发送444
5.3.11 第五个客户端发送555
5.3.12 服务端接收到所有客户端发送的信息
5.3.13 调试结果图总述
由上述结果可知:5个客户端向服务器发送了不同的信息并且都被服务器收到了。
本实验是从整体上对于网络协议的应用有了一个全面的认识。首先,在此之前我们没有接触过使用socket接口实现网络协议的一系列编程思想,在实验一总我们学会了socket中简单的协议的用法,服务端的建立,绑定,使用,监听等一系列的问题的研究已经从客户端连接服务端的各项内容。
从整体上对于winsock的使用流程有了更深一步的了解,通过阅读源码,对于TCP/UDP有了更深的理解。并能够深刻认识到二者直接的区别:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。