Windows环境下用C语言实现CS模型(基于TCP协议)

本篇博客是用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模型的缺点是显而易见的,下面我们逐条陈述:

  1. 一个accept只能接受一个客户端的连接,因此上述程序一个服务器只能连接一个客户端。
  2. 当服务器执行到accept函数时,如果没有客户端连接,程序会一直阻塞,无法继续执行。
  3. 当程序执行到recv函数时,如果没有接收到消息程序也会阻塞在这里,无法继续执行。
  4. 由于上述的缺点存在,当我们启动上述两个程序,客户端成功连接服务器后,该程序只能先由向客户端发信息,然后客户端回信息,然后再由服务器向客户端发信息,交替进行。
    为了克服上述缺点,人们提出了select模型,下一篇博客我们将介绍select模型的概念。
  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的基于TCP协议的客户端代码,使用标准C语言Windows环境实现: ```c #include <stdio.h> #include <stdlib.h> #include <winsock2.h> #define PORT 8080 #define SERVER_IP "127.0.0.1" int main() { WSADATA wsa; SOCKET s; struct sockaddr_in server; char message[1000], server_reply[2000]; // 初始化Winsock if (WSAStartup(MAKEWORD(2,2),&wsa) != 0) { printf("Failed. Error Code : %d",WSAGetLastError()); return 1; } // 创建Socket if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET) { printf("Could not create socket : %d" , WSAGetLastError()); } // 设置服务器地址和端口号 server.sin_addr.s_addr = inet_addr(SERVER_IP); server.sin_family = AF_INET; server.sin_port = htons(PORT); // 连接服务器 if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0) { printf("connect error"); return 1; } printf("Connected to server\n"); // 发送数据 printf("Enter message : "); fgets(message, 1000, stdin); if( send(s , message , strlen(message) , 0) < 0) { printf("Send failed"); return 1; } // 接收服务器的回复 if(recv(s , server_reply , 2000 , 0) < 0) { puts("recv failed"); } puts("Server reply :"); puts(server_reply); // 关闭Socket closesocket(s); WSACleanup(); return 0; } ``` 在这个例子中,我们使用了Windows Socket API(也称为Winsock)来实现TCP Socket通信。这个程序通过指定服务器的IP地址和端口号来连接到服务器,并发送一条消息。服务器接收到这条消息后,会回复一条消息,客户端再将回复输出到屏幕上。最后,客户端关闭Socket并清理Winsock资源。 需要注意的是,这个代码只是一个简单的示例,没有考虑错误处理和异常情况。在实际应用中,你需要根据自己的需求进行修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值