网络聊天程序的设计与实现

简介: 网络聊天程序的设计与实现

设计内容

了解 Socket 通信的原理,在此基础上编写一个聊天程序。使用 WinSock API 的编程,应该了解 TCP/IP 的基础知识。虽然你可以直接使用 WinSock API 来写网络应用程序,但是,要写出优秀的网络应用程序,还是必须对 TCP/IP 协议有一些了解的。WinSock 并不是一种网络协议,它只是一个网络编程接口,也就是说,它不是协议,但是它可以访问很多种网络协议,你可以把它当作一些协议的封装。现在的 WinSock 已经基本上实现了与协议无关。你可以使用 WinSock 来调用多种协议的功能。那么,WinSock 和 TCP/IP 协议到底是什么关系呢?实际上,WinSock 就是 TCP/IP 协议的一种封装,你可以通过调用 WinSock 的接口函数来调用 TCP/IP的各种功能.例如我想用 TCP/IP 协议发送数据,你就可以使用 WinSock 的接口函数 Send()来调用TCP/IP 的发送数据功能,至于具体怎么发送数据,WinSock 已经帮你封装好了这种功能。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 编程分为服务器端和客户端两部分,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) ;

来连接服务端。

下面是一个最简单的创建服务器端和客户端的例子:

服务器端的创建:

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 ); 

客户端的创建:

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 ) ); 

当服务器端和客户端建立连接以后,无论是客户端,还是服务器端都可以使用

int send( SOCKET s,const char FAR* buf,int len,int flags);

int recv( SOCKET s,char FAR* buf,int len,int flags);

函数来接收和发送数据,因为,TCP 连接是双向的。

当要关闭通讯连接的时候,任何一方都可以调用

int shutdown(SOCKET s,int how);

来关闭套接字的指定功能,再调用

int closeSocket(SOCKET s) ;

来关闭套接字句柄,这样一个通讯过程就算完成了。

可以参考下面的图1-1系统调用使用顺序图。

图1-1系统调用使用顺序图

注意:上面的代码没有任何检查函数返回值,如果你作网络编程就一定要检查任何一个 WinSock API 函数的调用结果,因为很多时候函数调用并不一定成功。上面介绍的函数,返回值类型是 int 的话,如果函数调用失败的话,返回的都是 SOCKET_ERROR。

设计步骤

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语言的多线程,在搜集了大量的网上资料之后,开始进行实验,然后最终选取的是在主函数调用_beginthread()方法开启多线程,但是在处理调试该方法传输的参数的时候出现了问题,因为当时没有了解很清楚,所以不知道参数的传递是不一样的,在服务端是需要传输accept()方法建立的和客户端有联系的sockConn,而客户端是需要传输connect()方法建立的和服务端有联系的sockClient。在仔细阅读理解之后就能够调试成功,同时我还加上聊天时间的显示调用了time()方法,这个调试过程也非常顺利。最后出现的问题就是关闭其中的一个进程,另一个进程会进入死循环,为了解决这个问题,我用了定义一个整型n接收recv()方法接收的值来做一个flag判断,当n<0时代表另一进程已经关闭。

设计结果及结果分析

聊天的过程大概是:客户端等待服务端成功响应之后,服务端首先会向客户端发送一个Welcome 客户端IP地址 to here!我们开始对话吧!然后客户端可以选择随时输入内容,服务端也可以选择随时输入内容,然后下面的整个对话是客户端向服务端发送了信息,然后服务端接收了客户端的信息之后又给客户端发送了信息,客户端因为有其他事情先关闭进程下线,服务端接收到客户端的下线信息之后可以选择关闭自己的进程下线。具体内容见下面图1-2、1-3。

图1-2客户端聊天界面

图1-3服务端聊天界面

源代码

服务器端

#include <stdio.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib") // winsock实现
#include <time.h>
#include <process.h>
//服务器端
void input(SOCKET sockConn)//服务器端输入新线程
{
    while(1)
    {
        char skh[50]="";
        printf("服务器输入:");
        scanf("%s",skh);
        send(sockConn,skh,strlen(skh),0);
    }
}
void main()
{
    WORD wVersionRequested;// 双字节,winsock库的版本
    WSADATA wsaData;// winsock库版本的相关信息
    int err;
    wVersionRequested = MAKEWORD( 1, 1 );// 0x0101 即:257
    err = WSAStartup( wVersionRequested, &wsaData );// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
    if ( err != 0 )//初始化winsock失败
    {
        return;
    }
    if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 )//判断版本匹配
    {
        WSACleanup( );
        return;
    }
    // AF_INET 表示采用TCP/IP协议族
    // SOCK_STREAM 表示采用TCP协议
    // 0是通常的默认情况
    SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);
    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//INADDR_ANY对本地任意ip地址,inet_addr("0.0.0.0")
    addrSrv.sin_family=AF_INET;// TCP/IP协议族
    addrSrv.sin_port=htons(6000);//socket对应的端口
    //sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址,与sockaddr_in二者长度一样
    bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
    listen(sockSrv,5);// 将socket设置为监听模式,5表示等待连接队列的最大长度
    //一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
    SOCKADDR_IN addrClient;
    int len=sizeof(SOCKADDR);
    SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);//第二个参数接收的是客户端的connect中的第二个参数server
    time_t t;
    time(&t);//聊天时间
    // sockSrv为监听状态下的socket
    // &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
    // len是包含地址信息的长度
    // 如果客户端没有启动,那么程序一直停留在该函数处
    // accept(sListen , (struct sockaddr*)&client , &iAddrSize)
    //服务器和客户端之间正常的传输
    char sendBuf[50];
    char pd;
    sprintf(sendBuf,"Welcome %s to here!我们开始对话吧!",inet_ntoa(addrClient.sin_addr));//inet_ntoa(addrClient.sin_addr)表示客户端的IP地址
    send(sockConn,sendBuf,strlen(sendBuf),0);// 发送数据到客户端,最后一个参数一般设置为0
    _beginthread(input,0,sockConn);
    while(1)
    {
        char recvBuf[50]="";//清空之前存储的数据
        int n=recv(sockConn,recvBuf,50,0);// 接收客户端数据,最后一个参数一般设置为0
        if(n<0)//客户端停止
        {
            printf("%s%s\n",ctime(&t),"客户端:");
            printf("%s","客户端已离线,您也可以选择退出关闭对话框下线!");
            _endthread();
            break;
        }
        printf("%s%s\n",ctime(&t),"客户端消息:");
        printf("%s\n",recvBuf);//输出服务器端从客户端接收的数据
    }
    closesocket(sockConn);
}

客户端

#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("客户端输入:");
        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( sClient , (struct sockaddr*)&server , sizeof( server ) )
    connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
    //服务器和客户端之间正常的传输
    time_t t;
    time(&t);//聊天时间
    _beginthread(input,0,sockClient);
    char beg[50]="Let's begin!";
    send(sockClient,beg,strlen(beg),0);
    while(1)
    {
        char recvBuf[50]="";//清空之前存储的数据
        int n=recv(sockClient,recvBuf,50,0);
        if(n<0)//服务器端停止
        {
            printf("%s%s\n",ctime(&t),"服务器:");
            printf("%s","服务器已离线,您也可以选择退出关闭对话框下线!");
            _endthread();
            break;
        }
        printf("%s%s\n",ctime(&t),"服务器消息:");
        printf("%s\n",recvBuf);
    }
    closesocket(sockClient);
    WSACleanup();
}


相关文章
|
1月前
|
存储 网络协议 Ubuntu
【C++网络编程】Socket基础:网络通讯程序入门级教程
【C++网络编程】Socket基础:网络通讯程序入门级教程
55 7
|
3天前
|
JSON 网络协议 调度
LabVIEW开发TCP网络通讯程序4
LabVIEW开发TCP网络通讯程序4
|
3天前
|
XML JSON 网络协议
LabVIEW开发TCP网络通讯程序3
LabVIEW开发TCP网络通讯程序3
|
3天前
|
网络协议 测试技术
LabVIEW开发TCP网络通讯程序2
LabVIEW开发TCP网络通讯程序2
|
3天前
|
网络协议
LabVIEW开发TCP网络通讯程序1
LabVIEW开发TCP网络通讯程序1
|
4天前
“无任何网络提供程序接受指定的网络路径”问题的几个解决方法
“无任何网络提供程序接受指定的网络路径”问题的几个解决方法
|
6天前
|
网络协议 Java Linux
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
12 0
|
6天前
|
存储 网络协议 算法
【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)
【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)
13 0
|
9天前
|
网络协议 物联网 数据处理
【C 言专栏】C 语言实现网络通信程序
【5月更文挑战第4天】本文探讨了使用C语言实现网络通信程序的方法,包括理解网络通信基本概念如协议和套接字,以及TCP/UDP通信的实现步骤。通过创建套接字、绑定端口、监听连接、数据传输和错误处理等关键环节,阐述了C语言在网络通信中的优势。文中还提到了实际应用案例、程序优化策略及未来发展趋势,旨在帮助读者掌握C语言在网络通信领域的应用技巧。
【C 言专栏】C 语言实现网络通信程序
|
24天前
|
存储 数据采集 NoSQL
使用Python打造爬虫程序之数据存储与持久化:从网络到硬盘的无缝对接
【4月更文挑战第19天】本文探讨了爬虫中的数据存储与持久化技术,包括文本文件存储、数据库(关系型与非关系型)、NoSQL数据库和键值存储,以及ORM框架的使用。根据数据类型、规模和访问需求选择合适存储方式,并注意数据安全、备份和恢复策略。正确选择和应用这些技术能有效管理和利用爬取数据。