本篇博客是用C语言实现基于Windows环境下的CS模型
最近在学习网络编程的相关知识,写下了这篇博客当随笔,如果你也在学习这方面的知识,希望可以帮到你;由于作者水平有限,如果本文中有不对的地方,欢迎在评论处指出。
服务端:
1.创建网络库并校验版本
2.创建socket函数
3.用bind函数绑定IP地址与端口号
4.用listen函数实现监听
5.用accept函数创建客户端链接
6.用recv函数与send函数与客户端收发数据
相比于服务端,客户端的程序较为简单:
客户端:
1.创建网络库并校验版本
2.创建socket函数
3.用connect函数连接服务器
4.用recv函数与send函数与服务器收发数据
下面介绍各个函数的使用方法:
1.打开网络库:
首先我们需要确定我们使用网络库的版本,然后调用WSAStartup函数。
WORD wdVersion = MAKEWORD(2, 2); //使用网络库的版本
WSADATA wdSockMsg; //系统通过这个参数给我们一些配置信息
int nRes = WSAStartup(wdVersion, &wdSockMsg); //打开网络库
//版本校验
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
//版本打开错误
WSACleanup(); //关闭网络库
return 0;
}
2.socket函数:
整个网络传输底层的协议体系非常复杂,而socket函数将执行流程进行了封装,socket函数就是我们调用协议体系进行通信的接口。每个客户端与服务器各有一个socket,通信的时候需要socket做参数,和谁通信就传谁的socket。
SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//创建服务器socket,这
//里第一个参数为IP地址类型(IPV4),第二个参数是套接字类型,第三个参数是协议类型(TCP)
3.bind函数:
bind函数用于绑定socket与地址和端口号,在网络传输中,一台电脑向另一台电脑传输信息时,首先通过IP地址找到另一台电脑,然后通过端口号找到另一台电脑相应的应用(QQ、微信等)。
struct sockaddr_in severMsg;
severMsg.sin_family = AF_INET; //地址类型
severMsg.sin_port = htons(12345); //端口号
severMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //IP地址
int bres = bind(socketSever, (const struct sockaddr*)&severMsg, sizeof(severMsg));
bind函数第一个参数是要绑定的socket(此处为服务器的socket),第二个参数为一个结构体的地址,结构体中包含地址类型、IP地址和端口号,第三个参数为参数2类型的大小。在这里我们需要强调参数2,在bind函数原型中用的是struct sockaddr结构体,但是struct sockaddr结构体赋值地址类型、IP地址和端口号极为不便,因此我们首先用struct sockaddr_in结构体分别赋值参数,然后将其强制类型转换为bind函数需要的类型。
在这里关于自己电脑端口号的问题:理论上我们电脑的端口号为0~65535,其中0-1024位系统预留端口号,我们不能使用,因此我们能使用的端口号通常较大,在这里为大家介绍两个函数如下:
netstat -ano //自己电脑已经被使用的端口号
netstat -ano|findstr "端口号" //查看某一个端口号是否被占用
使用方法:打开命令行窗口(win+R -> cmd),输入上面函数测试即可。
关于使用的IP地址,如果是两台电脑,这里服务器和客户端的IP地址都绑定服务器的IP地址就可以,但是限于硬件的限制,我们如果在同一台电脑上实验的话直接填“127.0.0.1”就可以,它是本地回环地址,用于本地网络测试。
4.listen函数(开始监听):
listen函数监听是否有客户端请求连接。
int a = listen(socketSever, SOMAXCONN);
参数1为socket,因为是服务器监听连接,所以绑定的是服务器的socket。参数2为请求等待队列的长度(如果一次有很多客户端请求连接服务器,服务器无法一次性处理全部请求就会将请求的信息加入一个队列,而第二个参数就是这个队列的长度),如果没有特殊情况,我们将第二个参数设置为SOMAXCONN即可,代表在系统允许的情况下将这个队列设置为最大。
5.accept函数:
accept函数允许在套接字上进行传入连接尝试。
listen函数监听客户端传来的连接,accept将客户端的信息绑定到一个socket上,也就是为客户创建一个socket,通过返回值返回给我们客户端的socket。当程序运行到accept函数处时会阻塞等待客户端传来的连接。一个accept函数只能接收来自一个客户端的连接。
struct sockaddr_in clientMsg;
int len = sizeof(clientMsg);
SOCKET socketClient = accept(socketSever, (struct sockaddr *)&clientMsg, &len);
accept函数第一个参数是服务器的socket,第二个参数和bind第二个参数格式类似,它用来将服务器的socket信息与第二个参数绑定,通过返回值返回给我们客户端的socket。
通过上面的步骤,只要服务器接收到客户端的连接,就会和客户端连接成功,下面我们通过recv与send函数与客户端实现收发信息。
6.send函数:
send函数用于向目标发送数据,它函数将我们的数据赋值粘贴进系统的协议发送缓冲区,计算机伺机发送出去(最大传输单元是1500字节)。
int send_a = send(socketClient, "I am sever, I have received your require;", sizeof("I am sever, I have received your require;"), 0);
第一个参数是对方(客户端)的socket;第二个参数是给对方发送的字符串,一般将其放在数组中;第三个参数是想要发送的字节的个数,第四个参数填0就OK。
7.recv函数:
得到指定客户端发来的消息;数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着接收到的数据。咱们外边调用的recv作用就是通过socket找到这个缓冲区,并把数据复制进参数2中。
//接收函数
char buf[1500] = { 0 };
int res = recv(socketClient, buf, 1499, 0);
recv函数第一个参数是对方的socket;第二个参数是一个字符数组,用于存储接收到的消息;第三个参数是想要读取的字节数,一般是参数字节数减1;第四个参数填0就OK。
8.connect函数
连接服务器并把服务器信息与服务器socket绑定在一起。
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int connect_a = connect(socketSever, (const struct sockaddr*)&si, sizeof(si));
connect函数的第一个参数为服务器的socket,第二个参数与bind函数的第二个参数类似,sockaddr结构体存储的地址类型、IP地址和端口号都是服务器的信息。
至此网络通信所用到的函数介绍完毕,下面我们附上完整的通信代码:
//服务端程序
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#pragma comment(lib,"ws2_32.lib")
#include<WinSock2.h>
#include<string.h>
int main()
{
WORD wdVersion = MAKEWORD(2, 2); //使用网络库的版本
WSADATA wdSockMsg; //系统通过这个参数给我们一些配置信息
int nRes = WSAStartup(wdVersion, &wdSockMsg); //打开/启动网络库,只有启动了这个库,库里的函数才能使用
if (0 != nRes) //如果打开网络库出错
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("可以重启电脑,或检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("Please reboot this software");
break;
case WSAEPROCLIM:
printf("请关闭不必要的软件,以为当前网络提供充足资源");
break;
case WSAEFAULT:
printf("参数错误");
break;
}
}
//版本校验
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))//LOBYTE是主,HIBYTE是副
{
//版本打开错误
WSACleanup(); //关闭网络库
return 0;
}
SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第一个参数是地址类型(IPV4),第二个参数是套接字类型,第三个参数是协议类型TCP
//如果执行失败则返回INVALID_SOCKET
if (INVALID_SOCKET == socketSever)
{
//如果socket调用失败
int a = WSAGetLastError(); //返回错误码
WSACleanup(); //关闭网络库
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
si.sin_port = htons(12345);
int bind_a = bind(socketSever, (struct sockaddr*)&si, sizeof(si));
/*参数1:前面创建的socket
参数2:是一个结构体sockaddr(包含地址类型、端口号和IP地址)地址,官方给出结构体sockaddr不方便赋值,因此我们定义sockaddr_in
分别赋值地址类型、端口号和IP地址后,强制类型转换为sockaddr
参数3:参数2类型的大小 */
if (SOCKET_ERROR == bind_a)
{
//bind函数出错
int a = WSAGetLastError(); //获得错误码
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
//开始监听
int listen_a = listen(socketSever, SOMAXCONN);
if (SOCKET_ERROR == listen_a)
{
//listen函数出错
int a = WSAGetLastError(); //获得错误码
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
//创建客户端链接
struct sockaddr_in clientMsg;
int len = sizeof(clientMsg);
SOCKET socketClient = accept(socketSever, (struct sockaddr*)&clientMsg, &len);//函数运行到accept函数会阻塞,等待客户端的连接
if (INVALID_SOCKET == socketClient)
{
//如果发生错误
printf("客户端连接失败\n");
int a = WSAGetLastError(); //返回错误码
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
printf("客户端连接成功\n");
while (1)
{
char buf[1500] = { 0 };
scanf("%s", buf);
int send_a = send(socketClient, buf, 1499, 0);
if (SOCKET_ERROR == send_a)
{
//如果出错
int a = WSAGetLastError(); //获得错误码
}
int recv_a = recv(socketClient, buf, 1499, 0);
if (0 == recv_a)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == recv_a)
{
printf("sever_recv错误码:%d", WSAGetLastError());
}
else
{
printf("传输内容:%s\n", buf);
}
}
closesocket(socketSever); //关闭服务端socket
closesocket(socketClient); //关闭客户端socket
WSACleanup(); //关闭网络库
return 0;
}
//客户端程序
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
WORD wdVersion = MAKEWORD(2,2); //使用的网络库版本
WSADATA wdSockMsg; //系统通过这个参数给我们一些配置信息
int nRes = WSAStartup(wdVersion, &wdSockMsg);
if (0 != nRes)
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("可以重启电脑,或检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("Please reboot this software");
break;
case WSAEPROCLIM:
printf("请关闭不必要的软件,以为当前网络提供充足资源");
break;
case WSAEFAULT:
printf("参数错误");
break;
}
return 0;
}
//版本校验
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
//版本打开错误
WSACleanup(); //关闭网络库
return 0;
}
//创建socket
SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketSever)
{
int a = WSAGetLastError(); //如果socket调用失败,返回错误码(工具 -> 错误查找)
WSACleanup(); //关闭网络库
return 0;
}
struct sockaddr_in clientMsg;
clientMsg.sin_family = AF_INET;
clientMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
clientMsg.sin_port = htons(12345);
int connect_a = connect(socketSever,(struct sockaddr*)&clientMsg,sizeof(clientMsg));
if (SOCKET_ERROR == connect_a)
{
//connect函数出错
printf("connect错误码:%d\n", WSAGetLastError());
closesocket(socketSever); //关闭socket
WSACleanup(); //清理网络库
return 0;
}
while (1)
{
char buf[1500] = { 0 };
//接收函数
int recv_a = recv(socketSever, buf, sizeof(buf), 0);
{
if (0 == recv_a)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == recv_a)
{
printf("recv错误码:%d\n", WSAGetLastError());
}
else
{
printf("传输内容:%s\n", buf);
}
}
//发送函数
scanf("%s", buf);
int send_a = send(socketSever, buf, 1499, 0);
if (SOCKET_ERROR == send_a)
{
//出现错误
int a = WSAGetLastError();
}
}
closesocket(socketSever);
return 0;
}
基本CS模型的缺点是显而易见的,下面我们逐条陈述:
- 一个accept只能接受一个客户端的连接,因此上述程序一个服务器只能连接一个客户端。
- 当服务器执行到accept函数时,如果没有客户端连接,程序会一直阻塞,无法继续执行。
- 当程序执行到recv函数时,如果没有接收到消息程序也会阻塞在这里,无法继续执行。
- 由于上述的缺点存在,当我们启动上述两个程序,客户端成功连接服务器后,该程序只能先由向客户端发信息,然后客户端回信息,然后再由服务器向客户端发信息,交替进行。
为了克服上述缺点,人们提出了select模型,下一篇博客我们将介绍select模型的概念。