计算机网络——网络聊天程序的设计与实现
一、实验目的
了解Socket通信的原理,在此基础上编写一个聊天程序。
二、总体设计
1. 基本原理
socket通信原理是一种“打开——读/写——关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方的内容,通讯结束时关闭文件。
Socket在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。
Socket保证了不同计算机之间的通信,对于网站,通信模型是服务器与客户端之间的通信。两端都建立一个socket对象,然后通过socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。
2. 设计步骤
(1)服务器端编程的步骤
①加载套接字库,创建套接字WSAStartup();
在使用socket之前要进行版本的设定和初始化,应用程序只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。根据版本初始化windows socket,返回0表示成功。
②创建套接字,使用TCP协议;
有套接字的接口才能进行通信。
③绑定套接字到一个 IP 地址和一个端口上(bind());
用bind()函数确定socket各种属性。
④将套接字设置为监听模式等待连接请求(listen());
⑤请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
accept()是一个阻塞函数,如果没有客户端请求,连接会一直等待在这里。该函数会返回一个新的套接字,这个新的套接字是用来与客户端通信的套接字,之前那个套接字是用来监听的套接字。
⑥用返回的套接字和客户端进行通信(send()/recv());
⑦返回,等待客户端的另一连接请求;
⑧关闭套接字,关闭加载的套接字库(closesocket())。
(2)客户端编程的步骤
①加载套接字库,创建套接字WSAStartup();
要连接的服务器的ip,因为现在服务器端就是本机,所以写本机ip,127.0.0.1一个特殊的IP地址,表示是本机的IP地址。
②向服务器发出连接请求(connect());
如果没有成功连接到服务器,一直循环,直至连接上为止。
③和服务器端进行通信(send()/recv());
④关闭套接字,关闭加载的套接字库(closesocket())。
三、详细设计
1. 程序流程图
2. 实验代码
(1)服务器端
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment (lib,"ws2_32.lib")
SOCKET sockConn;//全局变量,用来通信的套接字
//把发送消息和接收信息的功能封装成两个函数作为线程使用。
void recvFun();//接收信息线程
void sendFun();//发送信息线程
void createConnect();//创建通信套接字
int flag=0;
HANDLE h1, h2;//线程句柄
int main()
{
SOCKET serverSocket;//监视的套接字
//SOCKET sockConn;//用来通信的套接字
SOCKADDR_IN newAddr;//保存客户端的socket地址信息
SOCKADDR_IN addr;//地址结构体,包括ip port(端口)
WSADATA data;//存储被WSAStartup函数调用后返回的Windows Sockets数据
WORD version;//socket版本
int info;
//在使用socket之前要进行版本的设定和初始化
version = MAKEWORD(2, 2);//设定版本
info = WSAStartup(version, &data);
/*应用程序或DLL只能在一次成功的WSAStartup()调用之后
才能调用进一步的Windows Sockets API函数。
根据版本初始化 windows socket,返回0表示成功
*/
if (info != 0)
{
printf("初始化失败\n");
return 0;
}
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2)
{
printf("加载失败\n");
WSACleanup();
return 0;
}
//创建套接字,使用TCP协议
//有套接字的接口才能进行通信
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//AF_INET使用IPV4地址,SOCK_STREAM使用流传输,IPPROTO_TCP使用TCP协议
addr.sin_addr.S_un.S_addr = htonl(ADDR_ANY);//表示任何的ip过来连接都接受
addr.sin_family = AF_INET;//使用ipv4的地址
addr.sin_port = htons(0606);//设定应用占用的端口
//用bind()函数确定socket各种属性
bind(serverSocket, (SOCKADDR*)&addr, sizeof(SOCKADDR));//将套接字serverSocket与端口0606,接收的ip绑定
listen(serverSocket, 3);//开始监听,是否有客服端请求连接,最大连接数为3
printf("开始监听,等待客户端连接..........\n");
int len = sizeof(SOCKADDR);
//accept是一个阻塞函数,如果没有客户端请求,连接会一直等待在这里
//该函数会返回一个新的套接字,这个新的套接字是用来与客户端通信的套接字,之前那个套接字是监听的套接字
while(1){
sockConn=accept(serverSocket,(SOCKADDR*)&newAddr,&len);//接受客户端的请求
printf("连接成功......\n");
//创建线程后立即运行
//第一个参数表示线程内核对象的安全属性;第二个参数表示线程栈空间大小;第三个参数表示新线程所执行的线程函数地址(函数的名字),多个线程可以使用同一个函数地址
//第四个参数是传递给线程函数的参数;第五个参数指定什么时候调用线程,为0表示线程创建之后就可以进行调用;第六个参数返回线程的ID号,传入NULL表示不需要返回该线程ID号
h1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)sendFun, NULL, 0, NULL);//用于发送的线程
h2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)recvFun, NULL, 0, NULL);//用于接收的线程
}
closesocket(sockConn);//关闭套接字
return 0;
}
void sendFun()
{
char buf[128];
while (1)
{
printf("服务器端的信息:");
scanf("%s", buf);
//sockConn为用来通信的套接字,buf为发送数据的缓冲区,strlen(buf)+1为发送数据的长度,0位flags标志
send(sockConn, buf, strlen(buf) + 1, 0);//发送信息
}
}
void recvFun()
{
char buf[128];
while (1)
{
int Ret = recv(sockConn, buf, 128, 0);//接收信息
if(Ret<0){
printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
printf("客户端已退出,服务器端准备监听\n");
flag=flag%2+1;
break;
}else if(Ret==0){
printf("ERROR_RECV");
}else{
printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
printf("%s%s\n","客户端的信息: ", buf);
printf("服务器端的信息:");
}
}
}
(2)客户端
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
SOCKET sockClient;//全局变量,用于通信的socket
//两个线程用于接收和发送信息
void sendFun();//发送信息线程
void recvFun();//接收信息线程
int main()
{
HANDLE h1, h2;//线程句柄,其实就是一串数字用来标识线程对象
SOCKADDR_IN addr;
int info;
WSADATA data;
WORD version;
//设定版本,与初始化
version = MAKEWORD(2, 2);
info = WSAStartup(version, &data);
if (info != 0)
{
printf("初始化失败\n");
return 0;
}
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2)
{
printf("加载失败\n");
WSACleanup();
return 0;
}
sockClient = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
//要连接的服务器的ip,因为现在服务器端就是本机,所以写本机ip
//127.0.0.1一个特殊的IP地址,表示是本机的IP地址
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//端口要与服务器相同,不然找不到
addr.sin_port = htons(0606);
//用IPV4地址
addr.sin_family = AF_INET;
//主动连接服务器
while(1){
if(connect(sockClient, (SOCKADDR*)&addr, sizeof(SOCKADDR))==0)
break;
else
;
}
//创建线程后立即执行
h1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)sendFun, NULL, 0, NULL);
h2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)recvFun, NULL, 0, NULL);
WaitForSingleObject(h1, INFINITE);//会阻塞,直到线程运行结束
WaitForSingleObject(h2, INFINITE);
shutdown(sockClient,2);
closesocket(sockClient);//关闭套接字
return 0;
}
void sendFun()
{
char buf[128];
while (1)
{
printf("客户端的信息: ");
scanf("%s", buf);
//发送数据
send(sockClient, buf, strlen(buf) + 1, 0);
}
}
void recvFun()
{
char buf[128];
//接收服务发送的数据
while (1)
{
int n;
if(recv(sockClient, buf, 128, 0)>0){
printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
printf("%s%s\n","服务器端的信息: ", buf);
printf("客户端的信息:");
}
else{
break;
}
}
}