网络聊天程序的设计与实现 去年今日,是数据库的课程设计,用Java写一个管理系统。当时忙的不可开交,一边要写程序,一边要复习从来都没学过的几门课程。当时要复习概率论,可我一点都没看,那怎么办,就看猴博士复习。答题的时候答得一塌糊涂,成绩放榜,我概率论竟然考到了83分。此情此景,犹在脑海中。如今,2021年已至岁末,大三上学期的生活也接近尾声,可我依然没有进入到复习的状态。既然课程设计已经开始了,那就从此刻开始复习吧!
TCP/IP协议与WinSock网络编程接口的关系 WinSock 并不是一种网络协议,它只是一个网络编程接口,也就是说,它不是协议,但是它可以访问很多种网络协议,你可以把它当作一些协议的封装。现在的 WinSock 已经基本上实现了与协议无关。你可以使用 WinSock 来调用多种协议的功能。那么,WinSock 和 TCP/IP 协议到底是什么关系呢?实际上,WinSock 就是 TCP/IP 协议的一种封装,你可以通过调用 WinSock 的接口函数来调用 TCP/IP 的各种功能.例如我想用 TCP/IP 协议发送数据,你就可以使用 WinSock 的接口函数 Send()来调用
TCP/IP 的发送数据功能,至于具体怎么发送数据,WinSock 已经帮你封装好了这种功能。
TCP/IP协议介绍 TCP/IP 协议包含的范围非常的广,它是一种四层协议,包含了各种硬件、软件需求的定义。TCP/IP 协议确切的说法应该是 TCP/UDP/IP 协议。UDP 协议(User Datagram Protocol 用户数据报协议),是一种保护消息边界的,不保障可靠数据的传输。TCP 协议(Transmission Control Protocol 传输控制协议),是一种流传输的协议。他提供可靠的、有序的、双向的、面向连接的传输。
保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。
而面向流则是指无保护消息边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
举例来说,假如,我们连续发送三个数据包,大小分别是 2k、4k、8k,这三个数据包都已经到达了接收端的网络堆栈中,如果使用 UDP 协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完。而使用 TCP 协议,我们只要把接收的缓冲区大小设置在 14k 以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。
这就是因为 UDP 协议的保护消息边界使得每一个消息都是独立的。而流传输,却把数据当作一串数据流,它不认为数据是一个一个的消息。所以有很多人在使用 TCP 协议通讯的时候,并不清楚 TCP 是基于流的传输,当连续发送数据的时候,他们时常会认为 TCP 会丢包。其实不然,因为当它们使用的缓冲区足够大时,它们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其它据包却被忽略了。
WinSock编程简单流程 WinSock 编程分为服务器端和客户端两部分,TCP 服务器端的大体流程如下:
对于任何基于 WinSock 的编程首先必须要初始化 WinSock DLL 库。
int WSAStarup( WORD wVersionRequested,LPWSADATA lpWsAData )。
wVersionRequested 是我们要求使用的 WinSock 的版本。
调用这个接口函数可以初始化 WinSock 。
然后必须创建一个套接字(Socket)。
SOCKET Socket(int af,int type,int protocol);
套接字可以说是 WinSock 通讯的核心。WinSock 通讯的所有数据传输,都是通过套接字来完成的,套接字包含了两个信息,一个是 IP 地址,一个是 Port 端口号,使用这两个信息,就可以确定网络中的任何一个通讯节点。
当调用了 Socket()接口函数创建了一个套接字后,必须把套接字与你需要进行通讯的地址建立联系,可以通过绑定函数 bind 来实现这种联系。
int bind(SOCKET s,const struct sockaddr FAR* name,int namelen) ; struct sockaddr_in{ short sin_family ; u_short sin_port; struct in_addr sin_addr ; char sin_sero[8] ;
}
就包含了需要建立连接的本地的地址,包括地址族、IP 和端口信息。sin_family 字段必须把它设为 AF_INET,这是告诉 WinSock 使用的是 IP 地址族。sin_port 就是要用来通讯的端口号。sin_addr 就是要用来通讯的 IP 地址信息。
在这里,必须还得提一下有关’大头(big-endian)’小头(little-endian)’。因为各种不同的计算机处理数据时的方法是不一样的,Intel X86 处理器上是用’小头’形式来表示多字节的编号,就是把低字节放在前面,把高字节放在后面,而互联网标准却正好相反,所以,必须把主机字节转换成网络字节的顺序。
WinSock API 提供了几个函数。
把主机字节转化成网络字节的函数; u_long htonl(u_long hostlong); u_short htons(u_short hostshort);
把网络字节转化成主机字节的函数;
u_long ntohl(u_long netlong); u_short ntohs(u_short netshort) ;
这样,设置 IP 地址和 port 端口时,就必须把主机字节转化成网络字节后,才能用 Bind()函数来绑
定套接字和地址。
当绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求。 int listen(SOCKET s,int backlog); 这个函数可以把套接字转成监听模式。如果客户端有了连接请求,我们还必须使用
int accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen); 来接受客户端的请求。
现在基本上已经完成了一个服务器的建立,而客户端的建立的流程则是初始化 WinSock,然后创
建 Socket 套接字,再使用
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen) ; 来连接服务端。下面是一个最简单的创建服务器端和客户端的例子:
服务器端的创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 WSADATA wsd; SOCKET sListen; SOCKET sclient; UINT port = 800 ;int iAddrSize; struct sockaddr_in local , client ; WSAStartup( 0x11 , &wsd ); sListen = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ); local.sin_family = AF_INET; local.sin_addr = htonl( INADDR_ANY ); local.sin_port = htons( port ); bind( sListen , (struct sockaddr*)&local , sizeof ( local ) ); listen( sListen , 5 ); sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize );
客户端的创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 WSADATA wsd; SOCKET sClient; UINT port = 800 ;char szIp[] = "127.0.0.1" ;int iAddrSize; struct sockaddr_in server ; WSAStartup( 0x11 , &wsd ); sClient = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ); server.sin_family = AF_INET; server.sin_addr = inet_addr( szIp ); server.sin_port = htons( port ); connect( sClient , (struct sockaddr*)&server , sizeof ( server ) );
当服务器端和客户端建立连接以后,无论是客户端,还是服务器端都可以使用
1 2 int send ( SOCKET s,const char FAR* buf,int len,int flags) ;int recv ( SOCKET s,char FAR* buf,int len,int flags) ;
函数来接收和发送数据,因为,TCP 连接是双向的。当要关闭通讯连接的时候,任何一方都可以调用
1 int shutdown (SOCKET s,int how) ;
来关闭套接字的指定功能,再调用
1 int closeSocket (SOCKET s) ;
来关闭套接字句柄,这样一个通讯过程就算完成了。可以参考教材计算机网络(第 6 版)295 页图 6-32 所示的系统调用使用顺序:
注意:上面的代码没有任何检查函数返回值,如果你作网络编程就一定要检查任何一个 WinSock
API 函数的调用结果,因为很多时候函数调用并不一定成功。上面介绍的函数,返回值类型是 int 的话,如果函数调用失败的话,返回的都是 SOCKET_ERROR。
4.VC中socket编程步骤 sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),
原始套接字(SOCK_RAW);基于 TCP 的 socket 编程是采用的流式套接字。在这个程序中,将两个工程添加到一个工作区。要链接一个 ws2_32.lib 的库文件**(#pragma comment(lib,”ws2_32”))**。服务器端编程的步骤:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:绑定套接字到一个 IP 地址和一个端口上(bind()); 3:将套接字设置为监听模式等待连接请求(listen());
4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
5:用返回的套接字和客户端进行通信(send()/recv());
6:返回,等待另一连接请求;
7:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
服务器端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <Winsock2.h> void main () { WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1 , 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return ; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return ; } SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0 ); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000 ); bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof (SOCKADDR)); listen(sockSrv,5 ); SOCKADDR_IN addrClient; int len=sizeof (SOCKADDR); while (1 ){ SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len); char sendBuf[50 ]; sprintf (sendBuf,"Welcome %s to here!" ,inet_ntoa(addrClient.sin_addr)); send(sockConn,sendBuf,strlen (sendBuf)+1 ,0 ); char recvBuf[50 ]; recv(sockConn,recvBuf,50 ,0 ); printf ("%s\n" ,recvBuf); closesocket(sockConn); } }
客户端编程的步骤:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:向服务器发出连接请求(connect());
3:和服务器端进行通信(send()/recv());
4:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
客户端的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> #include <Winsock2.h> void main () { WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1 , 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return ; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return ; } SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0 ); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1" ); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000 ); connect(sockClient,(SOCKADDR*)&addrSrv,sizeof (SOCKADDR)); send(sockClient,"hello" ,strlen ("hello" )+1 ,0 ); char recvBuf[50 ]; recv(sockClient,recvBuf,50 ,0 ); printf ("%s\n" ,recvBuf); closesocket(sockClient); WSACleanup(); }
设计步骤 sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于 TCP 的 socket 编程是采用的流式套接字。在这个程序中,将两个工程添加到一个工作区。要链接一个 ws2_32.lib 的库文件(#pragma comment(lib,“ws2_32”))。 服务器端编程的步骤: 1:加载套接字库,创建套接字(WSAStartup()/socket()); 2:绑定套接字到一个 IP 地址和一个端口上(bind()); 3:将套接字设置为监听模式等待连接请求(listen()); 4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept()); 5:开启新线程调用_beginthread(input,0,sockConn); 6:用返回的套接字和客户端进行通信(send()/recv()); 7:返回,等待另一连接请求; 8:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。 客户端编程的步骤: 1:加载套接字库,创建套接字(WSAStartup()/socket()); 2:向服务器发出连接请求(connect()); 3:开启新线程调用_beginthread(input,0,sockClient); 4:和服务器端进行通信(send()/recv()); 5:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
调试过程 出师不利,由于忘记了进程的相互调用,直接将两份c文件放在了同一个工程文件夹里面。这就导致悲剧的发生,函数冲突。寻找到问题根源,苦思冥想,花费一个半小时寻求调试方法。最后看到操作系统课程设计的报告,方顿悟要将两份c文件编译成.exe文件,然后直接点击.exe文件,而不是在codeblocks里面调试。
在解决完这个问题后,我遇到了另一个问题,编译时出现undefined reference to 系列错误。查阅资料后,找到原因:缺少lib库,编译找不到相应的lib库。在这里贴上原作者解决问题的帖子:错误解决 .
线程为时分复用,可以极大提高程序效率,所以我们用多线程网络编程来处理。在主函数调用_beginthread()方法开启多线程,在服务端需要传输accept()方法建立的和客户端有联系的sockConn,而客户端需要传输connect()方法建立的和服务端有联系的sockClient。一番操作下来,程序成功运行。
设计结果及结果分析 将c文件编译成.exe文件,如图所示
接下来我们点击network2,再点击network,效果如图所示:
客户端键入消息:
服务器回应:
客户端下线:
客户端下线后:
完整代码 client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <stdio.h> #include <Winsock2.h> #pragma comment (lib,"ws2_32.lib" ) #include <time.h> #include <process.h> void input (SOCKET sockClient) { while (1 ) { char kh[50 ]="" ; printf ("%s\n" ,"可爱的客户端输入:" ); scanf ("%s" ,kh); send(sockClient,kh,strlen (kh),0 ); } }void main () { WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1 , 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return ; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return ; } SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0 ); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1" ); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000 ); connect(sockClient,(SOCKADDR*)&addrSrv,sizeof (SOCKADDR)); _beginthread(input,0 ,sockClient); char beg[50 ]="让我们开始愉快的唠嗑吧!\n" ; send(sockClient,beg,strlen (beg),0 ); while (1 ) { char recvBuf[50 ]="" ; int n=recv(sockClient,recvBuf,50 ,0 ); if (n<0 ) { printf ("%s\n" ,"威武的服务器:" ); printf ("%s" ,"威武的服务器已离线,您也可以选择退出关闭对话框下线!" ); _endthread(); break ; } printf ("%s\n" ,"威武的服务器消息:" ); printf ("%s\n" ,recvBuf); } closesocket(sockClient); WSACleanup(); }
server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <stdio.h> #include <Winsock2.h> #pragma comment(lib, "ws2_32.lib" ) #include <time.h> #include <process.h> void input (SOCKET sockConn) { while (1 ) { char skh[50 ]="" ; printf ("%s\n" ,"威武的服务器输入:" ); scanf ("%s" ,skh); send(sockConn,skh,strlen (skh),0 ); } }void main () { WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1 , 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return ; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return ; } SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0 ); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000 ); bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof (SOCKADDR)); listen(sockSrv,5 ); SOCKADDR_IN addrClient; int len=sizeof (SOCKADDR); SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len); char sendBuf[50 ]; char pd; sprintf (sendBuf,"欢迎 %s !我们开始唠嗑吧!\n" ,inet_ntoa(addrClient.sin_addr)); send(sockConn,sendBuf,strlen (sendBuf),0 ); _beginthread(input,0 ,sockConn); while (1 ) { char recvBuf[50 ]="" ; int n=recv(sockConn,recvBuf,50 ,0 ); if (n<0 ) { printf ("%s\n" ,"可爱的客户端:" ); printf ("%s" ,"可爱的客户端已离线,您也可以选择退出关闭对话框下线!" ); _endthread(); break ; } printf ("%s\n" ,"可爱的客户端消息:" ); printf ("%s\n" ,recvBuf); } closesocket(sockConn); }