2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

2-3 建立简易TCP服务端、客户端

文章目录

0-前言

【C++百万并发网络通信】系列是跟着B站up[夜作昼]的项目视频做的笔记[C++ socket 高并发 select异步 粘包 多线程 跨平台 日志 lua windows linux]

希望能通过博客的方式不断坚持学习,也希望偶然间看到这篇博客的你也能一起加油!

笔记目录:【C++百万并发网络通信-笔记目录】

更新时间:

2020.12.30 完成服务端简易功能

1-服务端简易功能

  • 建立socket
  • 绑定端口 bind
  • 监听网络端口 listen
  • 等待客户端连接 accept
  • 向客户端发送数据 send
  • 关闭socket closesocket

注意这里省略了【接收客户端数据】recv这一功能,即服务端只能【发】


2-客户端简易功能

  • 建立socket
  • 连接服务器 connect
  • 接收服务器信息 recv
  • 关闭socket closesocket

注意这里省略了【向服务端发送数据】send这一功能,即客户端只能【收】,这就好比我们有一部只能接电话的手机,不能打电话。


3-代码逻辑

#define WIN32_LEAN_AND_MEAN

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

#pragma comment(lib, "ws2_32.lib")//加入静态链接库

int main()
{
	WORD ver = MAKEWORD(2, 2);//WORD版本号
	WSADATA dat;//一种数据结构
	//启动windows socket 2.x环境
	WSAStartup(ver, &dat);
	//-------------------
	//--建立简易TCP客户端
	// 1 建立socket
	// 2 连接服务器 connect
	// 3 接收服务器信息 recv
	// 4 关闭socket closesocket
	//--建立简易TCP服务端
	// 1 建立socket
	// 2 绑定端口 bind
	// 3 监听端口 listen
	// 4 等待客户端连接 accept
	// 5 向客户端发送消息 send
	// 6 关闭socket closesocket
	//-------------------
	//清除Windows socket环境
	WSACleanup();//关闭windows socket网络环境
	return 0;
}

4-服务端

首先开始在当前【解决方案】下,新建一个【项目】EasyTcpServer,别忘了修改【输出目录】和【中间目录】,忘了可以看这里【VS2019新建项目、解决方案、多项目生成、防止文件污染】

重新生成新项目时,别忘了右键设为启动项目,如果报出下面的错误,就是#pragma那句忘了解注释,一定要解开如下:

2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

4-1 建立socket

// 1 建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

socket()函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。

函数声明:int socket( int af, int type, int protocol);

af:一个地址描述。仅支持AF_INET格式,也就是说ARPA Internet地址格式。AF_INET代表IPv4格式的网络地址。

type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM(基于流)、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

参考:百度百科

若无错误发生,socket()返回引用新套接口的描述字。下图中能够看到,SOCKET是一个uint类型的指针

2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

4-2 绑定端口

// 2 绑定端口 bind
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;//必须与建立socket的af保持一致,表示地址类型
_sin.sin_port = htons(4567);//host to net unsigned short,将主机端口转换为网络端口
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//随意ip地址//inet_addr("127.0.0.1");//本机地址,防止外网访问
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
	cout << "Error:绑定用于接收客户端连接的网络端口失败" << endl;
}
else
{
	cout << "Success:绑定网络端口成功..." << endl;
}

bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

函数声明:

bind(SOCKET s, const socketaddr *name, int namelen)

参数解释:

SOCKET s:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

const socketaddr *name:一个const struct sockaddr *指针,指向要绑定给SOCKET 的协议地址。

int namelen:对应的是地址的长度。

参考:socket–socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()、shutdown()

4-2-1 sockaddr

那么为什么bind()的第二个参数不直接使用sockaddr类型而要使用一个sockaddr_in类型再强制转换呢,这是因为sockaddr_in类型中的变量类型都是常用的变量,方便赋值。

sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了

struct sockaddr {  
     sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
   }; 

4-2-2 sockaddr_in

sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:

2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

参考:sockaddr和sockaddr_in详解

4-3 监听端口

// 3 监听端口 listen
if (SOCKET_ERROR == listen(_sock, 5))
{
	cout << "Error:监听网络端口失败" << endl;
}
else
{
	cout << "Success:监听网络端口成功..." << endl;
}

SOCKET_ERROR : 如调用bind()、listen()、connect()、send()、setsockopt()、fcntl()等函数时出错则会返回该宏:

参考:关于socket的各种错误码

函数声明:int listen (int sockfd, int backlog);

该函数在bind()之后accept()调用之前调用。第一个参数为已经创建的监听socket, 第二个参数是socket 监听队列最大监听连接数。

参考:Linux socket 编程API listen(SOCKET s, int backlog)

4-4 等待客户端连接

// 4 等待客户端连接 accept
sockaddr_in clientAddr = {};//远程客户端地址
int nAddrLen = sizeof(clientAddr);//结构长度
SOCKET _csock = INVALID_SOCKET;//无效的socket地址
_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);//核心
if (INVALID_SOCKET == _csock)
{
	cout << "Error:接收到无效客户端socket..." << endl;
}

使用accept()来接收客户端连接请求

函数声明:SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);

addr用于存放客户端的地址,addrlen在调用函数时被设置为addr指向区域的长度

参考:socket中accept()函数的理解

对于接收到的客户端socket,需要判断是否有效

4-5 发送数据

// 5 向客户端发送消息 send
char msgBuf[] = "Hello, I'm Server.";
send(_csock, msgBuf, strlen(msgBuf)+1, 0);

函数声明:int send(SOCKET s, const char *buf, int len, int flags);

SOCKET s:是本机要发送给谁的socket,本机是服务端,因此s就是要接收数据的客户端socket

const char *buf :应用程序要发送的数据的缓冲区(想要发送的数据)

int len:实际发送的字节数

int flags:一般置0

参考:Socket中send函数的理解

那么为什么int len的位置是strlen(msgBuf)+1呢,因为想把字符数组最后一位结束符也发过去

那么现在经过【4-4】与【4-5】已经能够实现单个客户端的接入,那么怎么实现不断接入客户端呢:需要加入一个循环,来不断接受来自客户端的连接请求。整理【4-4】与【4-5】代码如下,实现不断接入客户端,并为接入的客户端发送一条消息的功能。

// 4 等待客户端连接 accept
sockaddr_in clientAddr = {};//远程客户端地址
int nAddrLen = sizeof(clientAddr);//结构长度
SOCKET _csock = INVALID_SOCKET;//无效的socket地址
char msgBuf[] = "Hello, I'm Server.";
while (true)
{
	_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
	if (INVALID_SOCKET == _csock)
	{
		cout << "Error:接收到无效客户端socket..." << endl;
	}
	cout << "新客户端加入:IP = " << inet_ntoa(clientAddr.sin_addr) << endl;
	// 5 向客户端发送消息 send
	send(_csock, msgBuf, strlen(msgBuf) + 1, 0);
}

这里面,使用了一个比较老的函数inet_ntoa(),需要在程序开头定义一个宏

#define _WINSOCK_DEPRECATED_NO_WARNINGS

4-6 关闭socket

// 6 关闭socket closesocket
closesocket(_sock);

本函数关闭一个套接口。更确切地说,它释放套接口描述字s,以后对s的访问均以WSAENOTSOCK错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。

参考:百度百科


至此,完成TCP服务器的简易模型,生成项目成功

2-3 建立简易TCP服务端、客户端【socket server/client】【socket()、bind()、listen()、accept()、send()、closesocket()】

上一篇:aaaaaaazzzzzz


下一篇:Linux访问windows共享(samba/smbclient/smbfs/cifs)