C++ Socket 入门

Socket 入门

前置知识 :计算机网络基础(TCP/IP四层模型)

Socket 原意是“插座”,在计算机通信领域被翻译为“套接字”,以\(\{IP:Port\}\) 的形式表示。

Windows 与Linux 的Socket编程有一些小的区别,由于Unix系统中一切都是文件,网络连接也不例外,只要网络联通,一切的操作都是在操作文件,每个文件都会有一个文件描述符。与之相对应的,Windows下面的文件描述符就是句柄,利用句柄来操作Socket。本文总结了Windows的Socket编程

最常用的Socket主要是两种:

  1. 流式套接字(TCP) : SOCK_STREAM
    1. 数据在传输时不会丢失(可靠的)
    2. 数据按照顺序传输(先发送的先到达,后发送的后到达)
    3. 数据的发送和接受不是同步的(TCP以字节流的形式发送的特性)
  2. 数据包格式套接字(UDP) : SOCK_DGRAM
    1. 强调速度快而非顺序
    2. 传输的信息可能丢包也能损毁
    3. 限制每次传输的大小
    4. 数据的发送和接受是同步的

以上两种套接字的特性可以参考TCP协议与UDP协议

使用Socket通信的步骤

Server

  1. socket() 创建套接字
  2. bind() 套接字与\(\{IP:Port\}\) 进行绑定
  3. listen() 监听
  4. accept() 接受链接请求
  5. recv() 接受消息
  6. close() 关闭套接字

Client

  1. socket() 创建套接字
  2. connect() 向服务器发起连接
  3. send() 向服务器发送消息
  4. close() 关闭套接字

创建socket之前需要掌握的知识

动态库的加载

Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。

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

动态库的初始化

在使用Socket依赖的DDL之前,要先对其初始化,所用的函数是WSAStartup(),含义是\(Windows~Socket~API~Start~Up\), 要用该函数来指明WinSock的版本。

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

WORD 表示是一个字,两个字节16bit。wVersionRequested参数表示WinSock的版本号,我们可以用宏函数MAKEWORD 直接转换传入参数(版本号一般是最新的2.2)。

WSADATA 结构体存放有关动态库的信息,在函数执行完之后,会将信息放入到 lpWSAData

LP 表示指针 long point

综上,以下两行代码即可实现动态库的初始化:

WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);

创建套接字

Windows 下使用socket函数来创建套接字,返回SOCKET类型句柄

SOCKET socket(int af, int type, int protocol);
  1. af 地址族(Address Family)
    AF_INET,AF_INET6分别表示IPv4和IPv6,除了AF,也可以用PF(Protocol Family),BF_INET,BF_INET6

  2. type 套接字类型: SOCK_STREAM,SOCK_DGRAM。流式套接字以及数据包式套接字

  3. protocol 传输协议:IPPROTO_TCP, IPPROTO_UDP

Example:

SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);

第三个参数一般可以是0,前提是前面两个参数可以自行推断出第三个参数

绑定套接字 & 客户端与服务端连接

socket() 可以用来创建套接字,服务器要将套接字与\(\{IP:Port\}\) 进行绑定,之后,流经该指定IP的端口数据将由套接字处理。

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

sock 为SOCKET句柄,addr 为 sockaddr 结构体指针变量,addrlen 为 addr 变量大小,可以用 sizeof 运算符计算得到。

这里有一个重要的结构体 sockaddr, 该结构体内保存着IP地址类型以及地址和端口信息。由于不同的IP地址(IPv4 or IPv6),长度并不相同,为了方便传入地址信息,需要利用另外两个结构体。

Example:bind

// sockaddr_in 结构体用来存放地址信息
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充
sockAddr.sin_family = PF_INET;  //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址,由于127.0.0.1并不好存储,所以需要inet_addr进行转换
sockAddr.sin_port = htons(1234);  //端口,需要用htons函数转换

// 将sockaddr_in* 类型强制转换为SOCKADDR* 类型
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

上面的例子与IPv4的地址进行绑定,如果使用IPv6地址,需要将sockaddr_in 结构体换为:sockaddr_in6

sockAddr.sin_addr.s_addr 保存了IP地址,为什么不直接存在sin_addr里面,而要把s_addr装在sin_addr结构体里面? 大概是历史的原因

至此,服务端将socket与\(\{IP:Port\}\) 绑定成功,客户端可以通过connect 函数向服务端发起连接请求

int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);

Example: connect

//向服务器发起请求
sockaddr_in sockAddr;

memset(&sockAddr, 0, sizeof(sockAddr));  
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);

connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

接听与响应

服务端使用 listen 函数使套接字进入监听状态,在调用accept函数,以便随时响应连接请求

int listen(SOCKET sock, int backlog);
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);

backlog 为请求队列的长度,套接字处理客户端请求时,新的请求要放入到缓存队列中,该参数限制了队列的长度,如果设置为 SOMAXCONN ,则交给系统决定该长度

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept 函数中的addr保存了客户端的IP和端口信息

Example: listen & accept

//进入监听状态
listen(servSock, 20);

//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);

数据的接受与发送

!!! 终于到了有趣的地方了,前面所做的一切都是为了现在

发送数据使用:

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

接受数据使用:

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

这两个函数很简单,参数一目了然(最后的flags先不必管),buf表示要发送的字符串信息,len表示其长度。sock表示对方的socket句柄

服务器在accept到客户端的请求后,会获取到客户端的socket句柄,所以如果在服务器端向客户端发送数据或者接收数据就用该句柄

客户端在connect之前,创建的socket正是服务器的\(\{IP:Port\}\),利用该SOCKET句柄向服务器发送数据或者接收数据

实现回声客户端

Server端:

#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 100

int main() {

    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //创建套接字
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);

    //绑定套接字
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充
    sockAddr.sin_family = PF_INET;  //使用IPv4地址
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    sockAddr.sin_port = htons(1234);  //端口

    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //进入监听状态
    listen(servSock, 20);

    //接收客户端请求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);

    while (true) {
        char buffer[BUF_SIZE];
        int strLen = recv(clntSock, buffer, BUF_SIZE, 0);
        buffer[strLen] = '\0';
        if (strcmp(buffer, "quit") == 0) break;
        send(clntSock, buffer, strLen, 0);
    }

    //关闭套接字
    closesocket(clntSock);
    closesocket(servSock);
    //终止 DLL 的使用
    WSACleanup();
    return 0;
}

Client 端

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")  //加载 ws2_32.dll

#define BUF_SIZE 100

int main() {
    //初始化DLL    Windows Socket API DATA
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);


    //创建套接字
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    //向服务器发起请求
    sockaddr_in sockAddr;

    memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    while (true) {
        char bufSend[BUF_SIZE] = { 0 };
        printf("Input a string: ");
        scanf("%s", bufSend);
        send(sock, bufSend, strlen(bufSend), 0);
        if (strcmp(bufSend, "quit") == 0) {
            break;
        }
        char bufRecv[BUF_SIZE] = { 0 };
        recv(sock, bufRecv, BUF_SIZE, 0);

        //输出接收到的数据
        printf("Message form server: %s\n", bufRecv);
    }

    //关闭套接字
    closesocket(sock);

    //终止使用 DLL 
    WSACleanup();

    system("pause");
    return 0;
}

参考资料:http://c.biancheng.net/socket/

posted @ 2020-04-05 21:45  kpole  阅读(790)  评论(0编辑  收藏  举报