C++网络编程 零基础教程

C++网络编程 零基础教程

  • demo
  • 步骤
    • 框架
      • 一 服务端
      • 二、客户端
      • 伪代码
    • 详解
      • 0 套接字
      • 1 Windows 套接字下设置
      • 绑定端口
      • 监听
      • 接受连接请求
      • 收发数据
      • 关闭SOCKET
      • 客服端连接

demo

首先一个可以运行的例子:
windows环境下用c++实现socket编程

步骤

框架

一 服务端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、绑定套接字到一个IP地址和一个端口上(bind());

3、将套接字设置为监听模式等待连接请求(listen());

4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

5、用返回的套接字和客户端进行通信(send()/recv());

6、返回,等待另一个连接请求;

7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

二、客户端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、向服务器发出连接请求(connect());

3、和服务器进行通信(send()/recv());

4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

伪代码

服务器端

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

int main(int argc, char* argv[])
{
	初始化WSA(WSAStartup)
	创建套接字 (socket())
	绑定IP和端口 (sockaddr_in sin; bind())
	开始监听 (listen())
	whlie(true)  //循环接收数据  
	{
		接受连接请求 (sClient=accept())
		用返回的套接字和客户端进行通信(recv(sClient, revData, 255, 0);)
		发送数据 ( send(sClient, sendData, strlen(sendData), 0))
		关闭客户套接字 (closesocket(sClient);  )
	}
	关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup()return 0;
}

客户端

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

int main(int argc, char* argv[])
{
	初始化WSA(WSAStartup)
	while (true) {
		创建套接字 (socket(sclient,...))
		向服务器发出连接请求(connect())
		和服务器进行通信(send()/recv())
		关闭服务器接字 (closesocket(sclient);  )
	}
	闭加载的套接字库(WSACleanup()}

详解

0 套接字

1 Windows 套接字下设置

WSAStartup作用:打开网络库/启动网络库,启动了这个库,这个库里的函数/功能才能使用。
WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL)
使用DLL之前必须把DLL加载到当前程序,这里选择在编译时加载

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

调用 WSAStartup() 函数进行初始化

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

wVersionRequested: 调用程序使用windows socket的最高版本。 高字节指定小的版本号,低字节指定高的版本号。
lpWSAData:指向WSADATA数据结构体指针,接收Windows Socket的实现细节。

WSAStartup()函数以及DLL的加载
c++中Socket通信函数之WSAStartup

设置套接字
Windows 下也使用 socket() 函数来创建套接字,原型为:
SOCKET socket(int af, int type, int protocol);

  1. af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B127.0.0.1,它是一个特殊IP地址,表示本机地址
  2. type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM。
    • SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。

    • SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。

有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

该教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP协议
这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP协议
这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字

绑定端口

函数原型

int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
sockaddr_in 结构体
接下来不妨先看一下 sockaddr_in 结构体,它的成员变量如下:

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
  1. sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。

  2. sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。

  3. sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。

  4. sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。
    in_addr 结构体
    sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员,如下所示:

struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

监听

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
listen() 函数
通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。

请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。

接受连接请求

accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windows

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

收发数据

从服务器端发送数据使用 send() 函数,它的原型为:

int send(SOCKET sock, const char *buf, int len, int flags);

sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。

返回值和前三个参数不再赘述,最后的 flags 参数一般设置为 0 或 NULL,初学者不必深究。

在客户端接收数据使用 recv() 函数,它的原型为:

int recv(SOCKET sock, char *buf, int len, int flags);

关闭SOCKET

  1. 服务端关闭SOCKET
closesocket(Listen_Sock)
closesocket(Command_Sock)
  1. 客户端关闭SOCKET
closesocket(Client_Sock)

客服端连接

connect() 函数用来建立连接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

你可能感兴趣的:(c++)