计算机网络(四)Socket

目录

何为socket

使用socket

一、C/C++ socket API

1. 创建socket

2. 建立连接(仅限TCP)

3. 通信

4. 关闭socket

5. 服务器端完整示例

 5. 客户端完整示例

二、Socket的通信流程

1. TCP使用socket的通信流程

2. UDP使用socket的通信流程


何为socket

        Socket本身也是进程间通信方式的一种。但是与其他的方式不同,socket可以用于不同主机间的进程通信,这得益于它的创建方式和工作流程。在之前介绍TCP和UDP的文章中,端口作为首部中的一部分被提及,但并未进行详细的描述。实际上端口代表的就是一个进程,而一些特定的进程都有其固定的端口号,如DNS使用的默认端口号为53、通过HTTP向Web服务器请求网页的默认端口号为80等。而套接字的作用就是将端口与进程绑定,从而使发往指定端口的数据能够正确的被相应的进程接收。因此socket的本质就是传输层对应用层提供的接口,通常服务器端与客户端所使用的socket是不同的。在使用TCP时,服务器端会一直存在一个用于监听是否有客户端发起新的连接的socket,这种socket被称之为master socket;当确认需要建立连接时,双方便会各自新建一个用于传输数据的socket,这种socket则被称之为connected socket。在使用UDP时,不会建立真实的连接,而只是在收发数据时指定相应的地址和端口。

使用socket

        在进程使用socket进行各种操作时,就需要用到其暴露给应用层的各种接口,即socket API。在不同操作系统环境下使用不同语言操作socket的方法都各有不同,在这里主要介绍使用C/C++时的socket API。而通过TCP和UDP进行通信时,使用socket的流程也是不一样的。

一、C/C++ socket API

        在Linux和Windows环境下,使用socket时需要引入的头文件各不相同,使用的流程、API、数据类型和一些结构体的结构也有所不同,在具体使用时需要深入了解,这里只讨论在Windows环境下对Windows Socket 2的使用。在使用之前,需要在引入相关的静态库 (Ws2_32.lib,或是动态载入Ws2_32.ddl) 后再引入头文件winsock2.h。在操作socket之前,需要调用其中的WSAStartUp() 进行初始化,而在使用完Windows Socket 2之后,需要调用WSACleanup()进行收尾。想要了解更多Windows Socket 2中API的具体使用方法可以阅读官方文档,以下部分仅节选了几个常用的API进行简单的描述:

1. 创建socket

/*
    方法
*/

// 创建socket
SOCKET WSAAPI socket(
  [in] int af,                          // address family,指定地址簇的枚举
  [in] int type,                        // 指定socket类型的枚举
  [in] int protocol                     // 指定使用协议的枚举
);                                      // 若成功则返回描述符,否则返回INVALID_SOCKET

// 绑定 (无强制规定但通常只有客户端使用)
// 由于服务器端的进程通常使用固定端口,因此通常必须绑定
// 由于客户端的进程通常使用随机端口,因此通常不会绑定,否则需要确认当前端口是否已被占用
int WSAAPI bind(
  [in] SOCKET         s,                // 未绑定socket的描述符
  [in] const sockaddr *name,            // sockaddr结构体的位置,对不同地址簇其含义不同
  [in] int            namelen           // sockaddr结构体的长度
);                                      // 若成功则返回0,否则返回SOCKET_ERROR


/*
    简单示例
*/

// 声明变量
SOCKET sock;                            // 声明一个socket
struct sockaddr_in saServer;            // 声明一个sockaddr_in结构体
hostent* localHost;                     // 声明一个指向本地主机的指针
char* localIP;                          // 声明一个指向本地主机IP地址的指针

// 创建一个在TCP中使用IPv4地址簇的socket
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// 获取本地主机信息
localHost = gethostbyname("");
localIP = inet_ntoa (*(struct in_addr *)*localHost->h_addr_list);

// 更新sockaddr_in结构体
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(localIP);
saServer.sin_port = htons(5150);

// 使用sockaddr_in结构体中的内容绑定socket
// 传参时将sockaddr_in结构体类型转换成sockaddr结构体
bind(sock, (SOCKADDR*) & saServer, sizeof (saServer));

2. 建立连接(仅限TCP)

/*
    方法
*/

// 监听 (服务器端)
int WSAAPI listen(
  [in] SOCKET s,                        // 已绑定但未连接的socket的描述符
  [in] int    backlog                   // 挂起连接的队列的最大长度
);                                      // 若成功则返回0,否则返回SOCKET_ERROR

// 同意连接 (服务器端)
SOCKET WSAAPI accept(
  [in]      SOCKET   s,                 // 已开启监听的socket的描述符
  [out]     sockaddr *addr,             // 保存客户端信息的sockaddr结构体的位置,可选
  [in, out] int      *addrlen           // sockaddr结构体的长度,可选
);                                      // 若成功则返回一个已连接的新socket的描述符,
                                        // 否则返回INVALID_SOCKET

// 建立连接 (客户端)
int WSAAPI connect(
  [in] SOCKET         s,                // 已绑定但未连接的socket的描述符
  [in] const sockaddr *name,            // 包含服务器端信息的sockaddr结构体的位置
  [in] int            namelen           // sockaddr结构体的长度
);                                      // 若成功则返回0,否则返回SOCKET_ERROR


/*
    简单示例
*/

/* 服务器端 */

// 声明变量
SOCKET AcceptSocket;                    // 声明一个接受连接的socket

// 开始监听
// SOMAXCONN会将队列的最大长度置为最大的合理值
listen(sock, SOMAXCONN)

// 同意连接
// NULL表示不保存客户端信息
// 会阻塞进程直到收到客户端发起的连接
AcceptSocket = accept(sock, NULL, NULL);

/* 客户端 */

// 声明变量
struct sockaddr_in clientService;       // 声明一个sockaddr_in结构体

// 将服务器端的信息更新到sockaddr_in结构体中
clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr("127.0.0.1");
clientService.sin_port = htons(27015);

// 连接到指定的服务器端socket
// 传参时将sockaddr_in结构体类型转换成sockaddr结构体
connect(sock, (SOCKADDR *) & clientService, sizeof (clientService));

3. 通信

/*
    方法
*/

// 发送 (需要连接)
int WSAAPI send(
  [in] SOCKET     s,                    // 发送端socket的描述符
  [in] const char *buf,                 // 存有准备发送数据的缓存位置
  [in] int        len,                  // 要发送数据的字节数
  [in] int        flags                 // 指定调用方式的标志,用按位或运算得出,通常为0
);                                      // 若成功则返回实际发送的字节数,
                                        // 否则返回SOCKET_ERROR

// 接收 (需要连接)
int WSAAPI recv(
  [in]  SOCKET s,                       // 接收端socket的描述符
  [out] char   *buf,                    // 准备存放接收数据的缓存位置
  [in]  int    len,                     // 接收缓存的长度
  [in]  int    flags                    // 指定调用方式的标志,用按位或运算得出,通常为0
);                                      // 若成功则返回实际接收的字节数,
                                        // 否则返回SOCKET_ERROR

// 发送 (不需要连接)
int WSAAPI sendto(
  [in] SOCKET         s,                // 发送端socket的描述符
  [in] const char     *buf,             // 存有准备发送数据的缓存位置
  [in] int            len,              // 要发送数据的字节数
  [in] int            flags,            // 指定调用方式的标志,用按位或运算得出,通常为0
  [in] const sockaddr *to,              // 保存接收端信息的sockaddr结构体的位置,可选
  [in] int            tolen             // sockaddr结构体的长度,可选
);                                      // 若成功则返回实际发送的字节数,
                                        // 否则返回SOCKET_ERROR

// 接收 (不需要连接)
int WSAAPI recvfrom(
  [in]  SOCKET                 s,       // 接收端socket的描述符
  [out] char                   *buf,    // 准备存放接收数据的缓存位置
  [in]  int                    len,     // 接收缓存的长度
  [in]  int                    flags    // 指定调用方式的标志,用按位或运算得出,通常为0
  [out]               sockaddr *from,   // 保存接收端信息的sockaddr结构体的位置,可选
  [in, out, optional] int      *fromlen // sockaddr结构体的长度,可选
);                                      // 若成功则返回实际接收的字节数,
                                        // 否则返回SOCKET_ERROR


/*
    简单示例
*/

/* socket已连接时 */

// 声明变量
char SendBuf[1024];                     // 声明一个发送数据缓存
int SendBufLen = 1024;                  // 发送数据缓存长度
char RecvBuf[1024];                     // 声明一个接收数据缓存
int RecvBufLen = 1024;                  // 接收数据缓存长度

// 发送数据
send(sock, SendBuf, SendBufLen, 0);

// 接收数据
recv(sock, RecvBuf, RecvBufLen, 0);

/* socket无连接时,也可用于已连接的socket,此时最后两个参数为NULL */

// 声明变量
char SendBuf[1024];                     // 声明一个发送数据缓存
int SendBufLen = 1024;                  // 发送数据缓存长度
char RecvBuf[1024];                     // 声明一个接收数据缓存
int RecvBufLen = 1024;                  // 接收数据缓存长度
sockaddr_in RecvAddr;                   // 声明一个用于保存接收端信息的sockaddr_in结构体
sockaddr_in SendAddr;                   // 声明一个用于保存发送端信息的sockaddr_in结构体

// 将接收端的信息更新到sockaddr_in结构体中
RecvAddr.sin_family = AF_INET;
RecvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
RecvAddr.sin_port = htons(27015);

// 将服务器端的信息更新到sockaddr_in结构体中
SendAddr.sin_family = AF_INET;
SendAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
SendAddr.sin_port = htons(27015);

// 发送数据
sendto(sock, SendBuf, SendBufLen, 0, (SOCKADDR *) & RecvAddr, sizeof (RecvAddr));

// 接收数据
recvfrom(sock, RecvBuf, RecvBufLen, 0, (SOCKADDR *) & SendAddr, sizeof (SendAddr));

4. 关闭socket

/*
    方法
*/

// 关闭指定的socket功能,而不是socket本身,TCP会在传输结束后发送FIN报文段
int WSAAPI shutdown(
  [in] SOCKET s,                        // 准备关闭功能的socket的描述符
  [in] int    how                       // 准备关闭的功能的枚举
);                                      // 若成功则返回0,否则返回SOCKET_ERROR

// 关闭socket,TCP会直接发送FIN报文段,可能导致缓存区内数据丢失
int WSAAPI closesocket(
  [in] SOCKET s                         // 准备关闭的socket的描述符
);                                      // 若成功则返回0,否则返回SOCKET_ERROR


/*
    简单示例
*/

// 关闭socket的发送功能
shutdown(sock, SD_SEND);

// 关闭socket的接收功能
shutdown(sock, SD_RECEIVE);

// 关闭socket的所有功能
shutdown(sock, SD_BOTH);

// 关闭socket
closesocket(sock);

5. 服务器端完整示例

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015

int main() {

    //----------------------
    // 声明并初始化socket
    SOCKET ListenSocket = INVALID_SOCKET;
    SOCKET AcceptSocket = INVALID_SOCKET;

    // 声明sockaddr_in结构体并更新服务器端信息
    sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr("127.0.0.1");
    service.sin_port = htons(27015);

    // 声明并初始化测试用发送和接收缓存
    char *sendbuf = "This is a test from server";
    int recvbuflen = DEFAULT_BUFLEN;
    char recvbuf[DEFAULT_BUFLEN] = "";

    // 声明结果接收变量
    int iResult;

    //----------------------
    // 初始化Windows Socket 2
    WSADATA wsaData;
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != NO_ERROR) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    } else
        printf("Windows Socket 2 initialized.");

    //----------------------
    // 创建监听socket
    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) {
        printf("Socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    // 绑定监听socket
    if (bind(ListenSocket, (SOCKADDR *) & service, sizeof (service)) == SOCKET_ERROR) {
        printf("Bind failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // 开始监听
    if (listen(ListenSocket, 1) == SOCKET_ERROR) {
        printf("Listen failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    } else
        printf("Waiting for client to connect...\n");

    // 等待客户端发起连接后建立尝试连接
    AcceptSocket = accept(ListenSocket, NULL, NULL);
    if (AcceptSocket == INVALID_SOCKET) {
        printf("Accept failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    } else
        printf("Client connected.\n");

    //----------------------
    // 发送一个测试数据
    iResult = send(AcceptSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
       printf("Send failed with error: %d\n", WSAGetLastError());
        closesocket(AcceptSocket);
        WSACleanup();
        return 1;
    } else
        printf("Bytes sent: %d\n", iResult);

    // 发送完成后关闭发送功能
    iResult = shutdown(AcceptSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("Shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(AcceptSocket);
        WSACleanup();
        return 1;
    }

    // 持续接收数据直到对方发送完所有数据
    do {
        iResult = recv(AcceptSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0)
            printf("Bytes received: %d\n", iResult);
        else if (iResult == 0)
            printf("Connection closed.\n");
        else
            printf("recv failed with error: %d\n", WSAGetLastError());
    } while (iResult > 0);

    //----------------------
    // 关闭连接
    iResult = closesocket(AcceptSocket);
    if (iResult == SOCKET_ERROR) {
        printf("close failed with error: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    WSACleanup();
    return 0;
}

 5. 客户端完整示例

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015

int main() {

    //----------------------
    // 声明并初始化socket
    SOCKET ConnectSocket = INVALID_SOCKET;

    // 声明sockaddr_in结构体并更新服务器端信息
    sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr("127.0.0.1");
    service.sin_port = htons(27015);

    // 声明并初始化测试用发送和接收缓存
    char *sendbuf = "This is a test from client";
    int recvbuflen = DEFAULT_BUFLEN;
    char recvbuf[DEFAULT_BUFLEN] = "";

    // 声明结果接收变量
    int iResult;

    //----------------------
    // 初始化Windows Socket 2
    WSADATA wsaData;
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != NO_ERROR) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    } else
        printf("Windows Socket 2 initialized.");

    //----------------------
    // 创建socket
    ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ConnectSocket == INVALID_SOCKET) {
        printf("Socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    // 连接到服务器端
    iResult = connect(ConnectSocket, (SOCKADDR*) &service, sizeof(service));
    if (iResult == SOCKET_ERROR) {
        printf("Connect failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    } else
        printf("Server connected.");

    //----------------------
    // 发送一个测试数据
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
       printf("Send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    } else
        printf("Bytes sent: %d\n", iResult);

    // 发送完成后关闭发送功能
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("Shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    // 持续接收数据直到对方发送完所有数据
    do {
        iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0)
            printf("Bytes received: %d\n", iResult);
        else if (iResult == 0)
            printf("Connection closed.\n");
        else
            printf("recv failed with error: %d\n", WSAGetLastError());
    } while (iResult > 0);

    //----------------------
    // 关闭连接
    iResult = closesocket(ConnectSocket);
    if (iResult == SOCKET_ERROR) {
        printf("close failed with error: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    WSACleanup();
    return 0;
}

二、Socket的通信流程

        在了解了Windows环境下使用的Windows Socket 2中的一些常用API后,还需要简单地介绍一下TCP和UDP使用socket进行通信时的流程。

1. TCP使用socket的通信流程

        当使用TCP时,服务器端需要先调用WSAStartup()进行初始化,然后调用socket()和bind()创建一个服务器端的master socket并与端口绑定,再调用listen()来开启监听,最后调用accept()阻塞线程并等待客户端发起连接。而客户端则只需要使用socket()创建一个socket,不需要进行绑定。因为在发送数据时,未绑定的socket会被自动分配一个空闲的端口进行绑定。相反,若客户端选择手动绑定端口,则需要首先确认当前端口是否已被占用,使用起来更加麻烦。客户端在成功创建socket之后,需要调用connect()方法向服务器端发起连接,而服务器端阻塞的accept()在收到客户端发起的连接后新建一个socket与客户端的socket连接并进行通信。此时服务器端新建的socket和客户端的socket都为connected socket,通常服务器端的connected socket会被交给一个新开辟的线程进行管理。此时双方通常会调用send()和recv()来进行数据传递,不过sendto()和recvfrom()同样适用于这种情况,只是方法中的后两个参数会被置为NULL。当任何一方数据发送完成后,会调用shutdown()关闭socket的发送功能。当缓存中的所有数据发送完后,TCP会发送FIN报文段,而对方的recv()则会返回0。此时对方可以选择调用shutdown()关闭socket的接收功能,或是在已经关闭发送功能时选择直接调用closesocket()关闭socket。

2. UDP使用socket的通信流程

        当使用UDP时,服务器端在调用WSAStartup()进行初始化后,只需要使用socket()和bind()创建一个服务器端的socket并与端口绑定,就可以开始等待接收数据。客户端则和使用TCP时一样,只需要使用socket()进行创建,而不需要进行绑定。在双方socket都创建完成后,客户端可以选择通过sendto()和recvfrom()来直接进行数据的交互,也可以选择先调用connect()后再使用send()和recv()来进行数据的传递。但是与TCP中使用connect()时不同,UDP本身并不需要建立连接,也不会像TCP一样进行三次握手,因此调用connect()并不会与对方建立实际的连接,而只是创建了默认的目的地地址信息供send()和recv()使用。或者换句话说,调用connect()后,客户端的socket与服务器端的socket建立了传输层以上的、形式上的连接。而使用以上两种方式进行数据交互的不同在于,使用sendto()和recvfrom()来进行交互的底层是,每次的数据传输都需要经过建立连接、发送数据和关闭连接三个步骤;而在调用connect()后再使用send()和recv()来进行交互就不需要多次的建立和关闭连接,而是可以连续发送数据。当客户端完成数据的发送后,同样会调用shutdown()关闭socket的发送功能,再调用closesocket()关闭socket。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UCSB小学生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值