网络编程(一)网络编程入门

本节课学习TCP客户端和服务器端编程架构,其分为分为C/S(客户端/服务器模式)和B/S(浏览器/服务器架构模式)两种模式。接下来我们分别了解这两种模式

C/S模式

C/S模式:服务器首先先启动,并根据客户端请求做出相应的响应。

服务器:

1.打开一个通信通道,在某一端口接受客户端请求。

2.接受到客户端请求后处理请求并返回信息给客户端。

3.继续等待客户端请求直到关闭服务器。

客户端:

1.打开一个通信通道,并连接到服务器所在主机的特定端口。

2.向服务器发送请求,等待并接收响应,继续发送请求。

3.关闭客户端。

B/S模式

B/S模式:浏览器是客户端的主要应用软件,主要事物逻辑在服务器实现,前端(浏览器)负责展示。

WinSocket

Windows Sockets 规范是一套开放的、支持多协议的Windows下的网络编程接口。目前实际应用中的Windwos Sockets规范主要有1.1版本和2.2版本,其中1.1版本只支持TCP/IP协议,而2.2支持多协议,并具有良好的向后兼容性。这俩版本对应的头文件分别是:WinSocket.h WinSocket2.h

Socket传输

socket通常被称作套接字,是网络通信的编程接口,本质是​操作系统提供的双向通信端点​​。客户端去连接服务器端,需要一对套接字,一个运行在服务器端,一个运行在客户端。套接字处于网络协议的传输层,用于实现服务器和客户端之间的物理连接,并进行数据传输。传输层主要有UDP和TCP两个协议:

TCP协议:TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂

UDP协议:UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段

工作原理如下:

服务器端:创建套接字 → 绑定IP和端口 → 监听连接 → 接受客户端请求 → 数据传输 → 关闭连接

客户端:创建套接字 → 连接服务器 → 数据传输 → 关闭连接

相关背景知识

字节序:字节与存储位置的关系

小端:将低序字节存储在起始地址

大端:将高序字节存储在起始地址

网络字节序:网络上的字节序

代码实现

接下来我们将通过代码实现一个简单TCP/IP服务器和客户端的一般模型

服务端代码实现

服务端的实现有以下几个步骤:

1. 初始化Winsock

WORD wsVersion = MAKEWORD(2, 2);
WSADATA wsaData = {0}; 
WSAStartup(wsVersion, &wsaData);

2. 创建套接字

//创建套接字需要使用函数socket(),其语法如下:
SOCKET socket
(
    int af,       // 地址族规范:常见有IPv6(AF_INET6)或IPv4(AF_INET)
    int type,     // 套接字类型:原始套接字SOCKET_RAW(对较低层次的协议直接访问,例如IP、ICMP协议)、SOCK_STREAM面向连接(TCP/IP协议)、SOCK_DGRAM面向无连接(UDP协议)
    int protocol  // 使用的协议:这里我们可以直接写0,这样操作系统就会根据前面两个选项推断出你想用的协议
);

//实现代码
SOCKET sSocket = socket(AF_INET, SOCK_STREAM, 0);

3. 绑定套接字

//绑定套接字,需要使用函数bind,其语法如下:
int bind
(
    SOCKET s,                          // 套接字:将创建的套接字变量名字写上去
    const struct sockaddr FAR *name,   // 网络地址信息:包含通信所需要的相关信息,传递的是一个sockaddr结构体,在具体传参的时候,会用该结构体的变体sockaddr_in形式去初始化相关字段
    int namelen                        // sockaddr_in结构体的长度
);

//其中sockaddr_in结构体的定义如下:
struct sockaddr_in 
{
    short   sin_family; // 地址族规范:与创建套接字时候所使用的一致即可
    u_short sin_port; // 端口
    struct  in_addr sin_addr; // IP地址
    char    sin_zero[8]; // 无特殊的含义,只是为了与sockaddr结构体一致,因为在给套接字分配网络地址的时候会调用bind函数,其中的参数会把sockaddr_in结构体转化为sockaddr结构体
};

//其中in_addr结构体的定义如下:
struct in_addr 
{
    union 
    {
        struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
        struct { u_short s_w1,s_w2; } S_un_w;
        u_long S_addr;
    } S_un;
    #define s_addr  S_un.S_addr
                                /* can be used for most tcp & ip code */
    #define s_host  S_un.S_un_b.s_b2
                                /* host on imp */
    #define s_net   S_un.S_un_b.s_b1
                                /* network */
    #define s_imp   S_un.S_un_w.s_w2
                                /* imp */
    #define s_impno S_un.S_un_b.s_b4
                                /* imp # */
    #define s_lh    S_un.S_un_b.s_b3
                                /* logical host */
};

//网络地址是一个u_long类型的地址,因此我们可以使用函数inet_addr将字符串按照网络字节序进行转换
inet_addr("192.168.1.1");

//代码实现
sockaddr_in sockAddrInfo = {0};    // 初始化
sockAddrInfo.sin_addr.S_un.S_addr = inet_addr("192.168.1.1"); // 地址
sockAddrInfo.sin_port = htons(2118); // 端口需要按照网络字节序,所以需要使用htons函数
sockAddrInfo.sin_family = AF_INET; // 地址族规范
bind(sSocket, (sockaddr*)&sockAddrInfo, sizeof(sockAddrInfo));

4. 监听套接字

//监听套接字,使用函数listen,其语法如下:
int listen
(
  SOCKET s,    // 套接字:将创建的套接字变量名字写上去
  int backlog  // 待处理连接队列的最大长度:表示队列中最多同时有多少个连接请求
);
 
// 实现代码
listen(sSocket, 1);

5. 等待客户端连接

//等待连接,使用函数accept,其语法如下:
SOCKET accept
(
    SOCKET s, // 套接字:将创建的套接字变量名字写上去
    struct sockaddr FAR *addr, // 输出参数,需要传入一个sockaddr结构体的地址
    int FAR *addrlen // 输出参数,需要传入一个sockaddr结构体长度的地址
);
 
//实现代码,accept返回的也是一个SOCKET
sockaddr_in acceptSockAddrInfo = {0}; // 初始化
int acceptSockAddrLen = 0;
SOCKET aSocket = accept(sSocket, (sockaddr*)&acceptSockAddrInfo, &acceptSockAddrLen);

6. 接收和发送数据

//接收数据使用函数recv,其语法如下:
int recv
(
    SOCKET s,       // 套接字:将accept返回的套接字变量名字写上去
    char FAR *buf,  // 输出参数,数据缓冲区,接收到的数据
    int len,        // 缓冲区大小
    int flags       // 指定调用方式的标志,这个我们就直接写0即可
);
 
//实现代码
char buf[100] = {0};
recv(aSocket, buf, 100, 0);
printf("Recv data: %s\n", buf);

//发送数据使用函数send,其语法如下:

int send
(
    SOCKET s,             // 套接字:将accept返回的套接字变量名字写上去
    const char FAR *buf,  // 传输数据的缓冲区
    int len,              // 缓冲区大小
    int flags             // 指定调用方式的标志,这个我们就直接写0即可
);

//实现代码
send(aSocket, buf, strlen(buf)+1, 0);

7. 被动断开连接

//断开连接使用shutdown函数,其语法如下:
int shutdown
(
    SOCKET s,  // 套接字:将accept返回的套接字变量名字写上去
    int how    // 断开连接的形式:SD_SEND不再发送数据、SD_RECEIVE不再接受数据、SD_BOTH不再收发数据
);
 
// 实现代码
shutdown(aSocket, SD_SEND);

8.关闭套接字

//关闭2个套接字=使用函数closesocket,其语法如下:
int closesocket
(
    SOCKET s  // 套接字:将accept返回的套接字变量名字写上去
);
 
// 实现代码
closesocket(aSocket);
closesocket(sSocket);

 完整代码如下:

#include<WinSock2.h>//Winsock2头文件需置于Windows.h之前,避免旧版Winsock1.1头文件冲突
#include<Windows.h>
#include<iostream>
#include<WS2tcpip.h>

#pragma comment(lib,"ws2_32.lib")//显式链接Winsock2库ws2_32.lib

int main(int argc, char* argv[])
{
    // 1. 初始化Winsock
    WORD wsVersion = MAKEWORD(2, 2);
    WSADATA wsaData = {0};
    WSAStartup(wsVersion, &wsaData);
    // 2. 创建套接字
    SOCKET sSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (SOCKET_ERROR == sSocket) {
        printf("套接字闯创建失败!\n" );
    }
    else {
        printf("套接字闯创建成功!\n" );
    }
    3. 绑定套接字
    sockaddr_in sockAddrInfo = {0};    // 初始化
    sockAddrInfo.sin_addr.S_un.S_addr = inet_addr("172.16.176.5");
    sockAddrInfo.sin_port = htons(2118); // 端口
    sockAddrInfo.sin_family = AF_INET; // 地址族规范
 
    int bRes = bind(sSocket, (sockaddr*)&sockAddrInfo, sizeof(sockAddrInfo));
 
    if (SOCKET_ERROR == bRes) {
        printf("绑定失败!\n");
    }
    else {
        printf("绑定成功!\n");
    }
    // 3. 监听套接字
    int lRes = listen(sSocket, 1);
    if (SOCKET_ERROR == lRes) {
        printf("监听失败!\n");
    }
    else {
        printf("监听成功!\n");
    }
    4. 监听套接字
    sockaddr_in acceptSockAddrInfo = {0};    // 初始化
    int acceptSockAddrLen = sizeof(acceptSockAddrInfo);
    5. 等待客户端连接
    SOCKET aSocket = accept(sSocket, (sockaddr*)&acceptSockAddrInfo, &acceptSockAddrLen);
    if (INVALID_SOCKET == aSocket) {
        printf("服务端等待连接失败!\n");
    }
    else {
        printf("服务端等待连接成功!\n");
    }
    6. 接收和发送数据
    char buf[100] = {0};
    // 循环
    while (true) {
        int ret = recv(aSocket, buf, 100, 0);
        if (ret == 0) {
            // 如果recv返回为0则表示客户端要断开连接,就跳出循环断开连接
            break;
        }
        printf("Recv data: %s\n", buf);
        send(aSocket, buf, strlen(buf)+1, 0);
        memset(buf, 0, 100);
    }
    
    // 7. 断开连接
    shutdown(aSocket, SD_SEND);
    // 8.关闭套接字
    closesocket(aSocket);
    closesocket(sSocket);
    // 9. 释放 Winsock 库资源 
    WSACleanup();
    return 0;
}

客户端代码实现

客户端的实现有以下几个步骤:

1. 初始化Winsock

2. 创建套接字

3. 绑定套接字

4. 连接服务器

5. 发送和接收数据

6. 主动断开连接

7. 关闭套接字

#include<WinSock2.h>
#include<Windows.h>
#include<iostream>
#include<WS2tcpip.h>

#pragma comment(lib,"ws2_32.lib")

int main()
{
     // 1. 初始化Winsock
    WORD wsVersion = MAKEWORD(2, 2);
    WSADATA wsaData = {0};
    WSAStartup(wsVersion, &wsaData);
    // 2. 创建套接字
    SOCKET cSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (SOCKET_ERROR == cSocket) {
        printf("套接字闯创建失败!\n");
    }
    else {
        printf("套接字闯创建成功!\n");
    }
    // 3. 绑定套接字
    sockaddr_in sockAddrInfo = {0};    // 初始化
    sockAddrInfo.sin_addr.S_un.S_addr = inet_addr("172.16.176.12");
    sockAddrInfo.sin_port = htons(2119); // 端口
    sockAddrInfo.sin_family = AF_INET; // 地址族规范
    
    int bRes = bind(cSocket, (sockaddr*)&sockAddrInfo, sizeof(sockAddrInfo));
    
    if (SOCKET_ERROR == bRes) {
        printf("绑定失败!\n");
    }
    else {
        printf("绑定成功!\n");
    }
    // 4. 连接服务器
    sockaddr_in serverSockAddrInfo = {0};    // 初始化
    serverSockAddrInfo.sin_addr.S_un.S_addr = inet_addr("172.16.176.5");
    serverSockAddrInfo.sin_port = htons(2118); // 端口
    serverSockAddrInfo.sin_family = AF_INET; // 地址族规范
    int cRes = connect(cSocket, (sockaddr*)&serverSockAddrInfo, sizeof(serverSockAddrInfo));
    if (SOCKET_ERROR == cRes) {
        printf("与服务器连接失败!\n");
    }
    else {
        printf("与服务器连接成功!\n");
    }
    // 5. 发送和接收数据
    printf("Input: ");
    char sendData[100];
    scanf("%s", sendData);
    send(cSocket, sendData, strlen(sendData)+1, 0);
    char buf[100] = {0};
    recv(cSocket, buf, 100, 0);
    printf("Recv data: %s \n", buf);
    // 6. 主动断开连接
    shutdown(cSocket, SD_SEND);
    // 7. 关闭套接字
    closesocket(cSocket);
 
    WSACleanup();
    return 0;
}

多人聊天功能

在多人聊天中,有多个客户端对应一个服务器端。当某客户端发送消息时,由服务器端接收消息并转发给其他客户端

服务器端

#include<WinSock2.h>
#include<Windows.h>
#include<iostream>
#include<WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
#include<vector>

using std::vector;
vector<SOCKET> clientList;//用于存储多个客户端

DWORD WINAPI revcMeesage(LPVOID param)
{
	SOCKET client = (SOCKET)param;
	char buff[0x100]{ 0 };
	int recvSize = 0;
	while ((recvSize=recv(client, buff,0x100,0))>0)
	{
		for (int i=0;i<clientList.size();i++)
		{
			if (clientList[i]!= client)
			{
				send(clientList[i],buff, recvSize,0);//服务器发送给其他客户端
			}
		}
	}

	for (int i = 0; i < clientList.size(); i++)
	{
		if (clientList[i] == client)
		{
			closesocket(clientList[i]);
			clientList.erase(clientList.begin()+i);
			printf("客户端%u断开了连接!\n");
		}
	}
	return 0;
}
int main()
{
	//1.初始化网络环境
	WSADATA data{ 0 };
	WSAStartup(MAKEWORD(2, 2), &data);
	//2.创建SOCKET
	SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//3绑定端口号IP地址
	sockaddr_in serverAddr{ 0 };
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8888);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	bind(server, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
	//4.监听
	listen(server, SOMAXCONN);
	//5.接受会话
	sockaddr_in clientAddr{ 0 };
	int size = sizeof(clientAddr);
	while (1)
	{
		SOCKET client = accept(server, (SOCKADDR*)&clientAddr, &size);
		clientList.push_back(client);
		CreateThread(NULL,NULL, revcMeesage, (LPVOID)client,NULL,NULL);
		printf("客户端%u连接到了服务器\n", client);
	}
	closesocket(server);
	return 0;
}

客户端

#include<Windows.h>
#include<iostream>
#include<WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
DWORD WINAPI revcMeesage(LPVOID param)
{
	SOCKET client = (SOCKET)param;
	char buff[0x100]{ 0 };
	while (recv(client, buff, 0x100, 0)>0)
	{
		printf("%s\n",buff);
	}
	return 0;
}

int main()
{
	//1.初始化网络环境
	WSADATA data{ 0 };
	WSAStartup(MAKEWORD(2, 2), &data);
	//2.创建SOCKET
	SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//3绑定端口号IP地址
	sockaddr_in serverAddr{ 0 };
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8888);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	int result = connect(client, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
	if (result != 0)
	{
		printf("连接服务器失败!\n");
	}
	HANDLE hthread=CreateThread(NULL, NULL, revcMeesage, (LPVOID)client, NULL, NULL);
	char buff[0x100]{0};
	while (scanf_s("%s", buff,0x100) && strcmp(buff,"exit"))
	{
		send(client,buff,strlen(buff)+1,0);
	}
	closesocket(client);
	CloseHandle(hthread);
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值