目录

  1. 1. 前言
  2. 2. 数据结构
    1. 2.1. 本机字节顺序和网络字节顺序
  3. 3. 面向连接的套接字编程
    1. 3.1. C/S实例
      1. 3.1.1. 服务端
      2. 3.1.2. 客户端
    2. 3.2. 改进
  4. 4. 利用流式套接字实现文件传送
    1. 4.1. 服务端
    2. 4.2. 客户端
  5. 5. 无连接的套接字编程
    1. 5.1. C/S实例
      1. 5.1.1. 服务端
      2. 5.1.2. 客户端

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

网络协议程序设计

2025/3/7
  |     |   总文章阅读量:

前言

学学协议设计,加深下理解(?


数据结构

sockaddr 结构:针对各种通信域的套接字,存储它们的地址信息

struct sockaddr {
	u_short	sa_family;	// 地址家族
	char	sa_data[14];	// 协议地址
};

sockaddr_in 结构:专门针对 Internet 通信域,存储套接字相关的网络地址信息,例如 IP 地址,传输层端口号

官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-sockaddr_in

struct sockaddr_in {
	short	sin_family;	// 地址家族
	u_short	sin_port;	// 端口号
	struct in_addr	sin_addr;	// IP地址
	char	sin_zero[8];	// 全为0
};

in_addr 结构:专门用来存储 IP 地址

typedef 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;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

用法:

首先定义一个 sockaddr_in 的结构实例,并将它清零;

然后再进行赋值;

函数调用时将这个结构强转为 sockaddr

struct sockaddr_in myad;
memset(&myad,0,sizeof(struct sockaddr_in));
myad.sin_family = AF_INET;
myad.sin_port = htons(8080);
myad.sin_addr.S_addr = htonl(INADDR-ANY);

accept(listenfd, (sockaddr*)(&myad), &addrlen);

本机字节顺序和网络字节顺序

本机字节顺序:在具体计算机中的多字节数据的存储顺序

网络字节顺序:多字节数据在网络协议报头中的存储顺序,套接字中必须用

所以装入套接字时要从本机字节顺序转为网络字节顺序,本机输出时要从网络字节顺序转换为本机字节顺序

  • htons():短整数本机顺序转换为网络顺序,用于端口号
  • htonl():长整数本机顺序转换为网络顺序,用于IP地址
  • ntohs():短整数网络顺序转换为本机顺序,用于端口号
  • ntohl():长整数网络顺序转换为本机顺序,用于IP地址

面向连接的套接字编程

image-20250319172427824

服务端:

  • SOCKET():初始化一个 socket

    WINSOCK_API_LINKAGE SOCKET WSAAPI socket(int af,int type,int protocol);

    af:协议簇,如 PF_INET 表示 TCP/IP

    #define AF_INET 2
    
    #define PF_INET AF_INET

    type:通信类型

    #define SOCK_STREAM 1	// 面向连接
    #define SOCK_DGRAM 2	// 无连接
    #define SOCK_RAW 3
    #define SOCK_RDM 4
    #define SOCK_SEQPACKET 5

    protocol:传输层协议,在 Internet 通信域中,一般取0,由系统决定

  • BIND():socket 绑定网络地址

    WINSOCK_API_LINKAGE int WSAAPI bind(SOCKET s,const struct sockaddr *name,int namelen);

    s:套接字描述符

    *name:指针所指结构中保存着特定的网络地址,Internet 域中,网络地址 = IP 地址 + 传输层端口号

    namelen:sockaddr 的结构长度,等于 sizeof(struct sockaddr)

  • LISTEN():监听

    WINSOCK_API_LINKAGE int WSAAPI listen(SOCKET s,int backlog);

    s:套接字描述符

    backlog:等待连接队列的最大长度,最大为20,一般为5~10;如果缓冲区队列有空,就接收一个来自客户端的连接请求,否则拒绝

  • ACCEPT():接受客户端的连接请求

    WINSOCK_API_LINKAGE SOCKET WSAAPI accept(SOCKET s,struct sockaddr *addr,int *addrlen);

    s:套接字描述符

    *addr:出口参数,调用完毕后指针放置客户端的网络地址

    *addrlen:出口参数,调用时初始设置 addr 结构的长度,调用完毕返回客户端的网络地址长度

  • READ():读取客户端发送来的请求/命令数据

  • WRITE():向客户端发送响应数据

  • CLOSE():关闭 socket

客户端:

  • SOCKET()

  • CONNECT():向服务端发送连接请求

    WINSOCK_API_LINKAGE int WSAAPI connect(SOCKET s,const struct sockaddr *name,int namelen);

    类似于bind

  • WRITE()

  • READ()

  • CLOSE()

C/S实例

stdafx.h:预编译头,是自定义的头文件,与项目的源代码文件存放在同一个文件夹,内容可以包含系统的头文件如 stdio.h、string.h 等,这样就不用重复引入了

#include <winsock2.h>
#include <Windows.h>

#include <stdio.h>
#include <string.h>

#pragma comment(lib,"xxx.lib"):引入库文件,用于调用动态链接库;这里用于引入 winsock2.h 的库 ws2_32.lib,注意编译时要添加-lws2_32选项


WSADATA:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-wsadata

初始化 winsock:https://learn.microsoft.com/zh-cn/windows/win32/winsock/initializing-winsock

服务端

定义完数据和套接字后就可以填入对应参数,依次进行 bind、listen、accept 等操作

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;

    SOCKET sockServer;
    SOCKADDR_IN addrServer;

    SOCKET sockClient;
    SOCKADDR_IN addrClient;

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

    sockServer = socket(AF_INET, SOCK_STREAM, 0);

    addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = htons(6000);

    bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

    listen(sockServer, 5);
    printf("服务器已启动:\n监听中...\n");

    int len = sizeof(SOCKADDR);
    char recvBuf[100];

    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);

    recv(sockClient, recvBuf, 100, 0);
    printf("%s\n", recvBuf);

    
    closesocket(sockClient);

    WSACleanup();

    return 0;
}

编译:

gcc server.c -o output/server.exe -lws2_32 -fexec-charset=GBK

image-20250310152151388

客户端

注意,c语言中为了确保字符串的终止符\0(空字符)也被发送,send 发送到服务端的长度需要 +1

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;

    SOCKET sockClient;
    SOCKADDR_IN addrServer;

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

    sockClient=socket(AF_INET,SOCK_STREAM,0);

    addrServer.sin_family=AF_INET;
    addrServer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
    addrServer.sin_port=htons(6000);


    connect(sockClient,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));

    char message[30]="111 Hello Socket!";
    send(sockClient,message,strlen(message)+1,0);

    closesocket(sockClient);
    WSACleanup();

    return 0;
}

image-20250310152403062


改进

  1. 在服务器显示连接上的客户机的 IP 地址和端口

    服务端在 accept 的时候保留了客户端的信息 addrClient,只需要将其转格式输出即可

    使用 inet_ntoainet_addr的网络字节序转换为 IP 地址,ntohs对应客户端的htons

    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
    printf("客户端 %s:%d\n",inet_ntoa(addrClient.sin_addr),ntohs(addrClient.sin_port));

    image-20250310160618580

  2. 服务器也发送一条信息(Hello! Client Socket.)给客户机并显示

    服务端:和客户端发给服务端消息一样的操作

    char message[30] = "Hello! Client Socket.";
    send(sockClient, message, strlen(message) + 1, 0);
    printf("消息已发送给客户端\n");

    客户端:和服务端接收消息一样的操作,从 sockClient 接收

    char recvBuf[100];
    recv(sockClient, recvBuf, 100, 0);
    printf("收到服务端消息:%s\n", recvBuf);

    image-20250310161531630

  3. 完成步骤 2 后,修改为:

    • 客户机键盘输入某一数字(例如 5)并将其发送给服务器
    • 服务器收到数字并显示,然后将该数字加 1(例如 5+1=6)并将结果回送给客户机
    • 客户机收到数字并显示

    先修改客户端发送逻辑:

    int input;
    printf("请输入一个整数: ");
    scanf("%d", &input);
    send(sockClient, (char*)&input, sizeof(input), 0);

    服务端接收逻辑:

    int receivedNum;
    recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
    printf("收到客户端数字: %d\n", receivedNum);

    接下来修改服务端发送逻辑:

    int resultNum = receivedNum + 1;
    send(sockClient, (char*)&resultNum, sizeof(resultNum), 0);

    客户端接收逻辑:

    int receivedNum;
    recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
    printf("收到服务端数字: %d\n", receivedNum);

    image-20250310163205389

  4. 修改程序使得以下情况能正常运行:服务器程序不关闭,客户机程序可重复运行关闭,两端正常接收和发送消息

    直接 while True 保持运行即可

    服务端需要持续运行的部分从 accept 开始,客户端则是从建立 socket 开始

    服务端完整代码:

    #include "stdafx.h"
    #pragma comment(lib, 'ws2_32.lib')
    
    int main(int argc, char const *argv[])
    {
        WSADATA wsaData;
    
        SOCKET sockServer;
        SOCKADDR_IN addrServer;
    
        SOCKET sockClient;
        SOCKADDR_IN addrClient;
    
        int len = sizeof(SOCKADDR);
    
    
        WSAStartup(MAKEWORD(2, 2), &wsaData);
    
        sockServer = socket(AF_INET, SOCK_STREAM, 0);
    
        addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        addrServer.sin_family = AF_INET;
        addrServer.sin_port = htons(6000);
    
        bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    
        listen(sockServer, 5);
        printf("服务器已启动...\n监听中...\n");
    
    
        while(1){
            sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
            printf("客户端 %s:%d\n", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port));
        
            // char recvBuf[100];
            // recv(sockClient, recvBuf, 100, 0);
            // printf("收到客户端消息:%s\n", recvBuf);
            int receivedNum;
            recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
            printf("收到客户端数字: %d\n", receivedNum);
        
            // char message[30] = "Hello! Client Socket.";
            // send(sockClient, message, strlen(message) + 1, 0);
            int resultNum = receivedNum + 1;
            send(sockClient, (char*)&resultNum, sizeof(resultNum), 0);
            printf("已返回结果\n");
    
            closesocket(sockClient);
            printf("客户端连接已关闭,等待下一个...\n");
        }
    
        closesocket(sockClient);
        WSACleanup();
        return 0;
    }

    客户端完整代码:

    #include "stdafx.h"
    #pragma comment(lib, 'ws2_32.lib')
    
    int main(int argc, char const *argv[])
    {
        WSADATA wsaData;
    
        SOCKET sockClient;
        SOCKADDR_IN addrServer;
    
        int input;
        int receivedNum;
    
        WSAStartup(MAKEWORD(2, 2), &wsaData);
    
        while (1)
        {
            sockClient = socket(AF_INET, SOCK_STREAM, 0);
    
            addrServer.sin_family = AF_INET;
            addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
            addrServer.sin_port = htons(6000);
    
            if (connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR)) == SOCKET_ERROR)
            {
                printf("连接失败");
                continue;
            }
    
            // char message[30] = "111 Hello Socket!";
            // send(sockClient, message, strlen(message) + 1, 0);
    
            printf("请输入一个整数 (输入-1退出): ");
            scanf("%d", &input);
            if (input == -1)
                break;
    
            send(sockClient, (char *)&input, sizeof(input), 0);
    
            // char recvBuf[100];
            // recv(sockClient, recvBuf, 100, 0);
            // printf("收到服务端消息:%s\n", recvBuf);
            recv(sockClient, (char *)&receivedNum, sizeof(receivedNum), 0);
            printf("收到服务端数字: %d\n", receivedNum);
        }
    
        closesocket(sockClient);
        WSACleanup();
        return 0;
    }

    image-20250310164459188

  5. 一台服务器,多台客户机的情况下,由于服务端只能同时处理一个端口上的连接,会出现其他客户端等待的情况

    image-20250310164744022

    在 UNIX 系统下,可以使用fork()创建子进程解决这个问题


利用流式套接字实现文件传送

使用 VC++ 6.0

创建 Win32 Console Application 工程

image-20250319193325558

服务端

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
    //先检查文件是否存在
    char *filename = "C:\\test.txt";  //文件名
    FILE *fp = fopen(filename, "rb");  //以二进制方式打开文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sockAddr;
    
    memset(&sockAddr, 0, sizeof(sockAddr));
    
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    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);
    
    //循环发送数据,直到文件结尾
    char buffer[BUF_SIZE] = {0};  //缓冲区
    int nCount;
    while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
        send(clntSock, buffer, nCount, 0);
    }
    shutdown(clntSock, SD_SEND);  //文件读取完毕,断开输出流,向客户端发送FIN包
    recv(clntSock, buffer, BUF_SIZE, 0);  //阻塞,等待客户端接收完毕
    fclose(fp);
    
    closesocket(clntSock);
    closesocket(servSock);
    WSACleanup();
    system("pause");
    return 0;
}

客户端

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int main(){
    //先输入文件名,看文件是否能创建成功
    char filename[100] = {0};  //文件名
    printf("Input filename to save: ");
    gets(filename);
    FILE *fp = fopen(filename, "wb");  //以二进制方式打开(创建)文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sockAddr;
    
    memset(&sockAddr, 0, sizeof(sockAddr));
    
    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));
    
    //循环接收数据,直到文件传输完毕
    char buffer[BUF_SIZE] = {0};  //文件缓冲区
    int nCount;
    while( (nCount = recv(sock, buffer, BUF_SIZE, 0)) > 0 ){
        fwrite(buffer, nCount, 1, fp);
    }
    puts("File transfer success!");
    //文件接收完毕后直接关闭套接字,无需调用shutdown()
    fclose(fp);
    
    closesocket(sock);
    WSACleanup();
    system("pause");
    return 0;
}

f7 编译,crtl+f5 运行

image-20250319200515060

image-20250319200553008

成功下载文件 test.txt 并保存为 save.txt


无连接的套接字编程

image-20250319172252531

特点:

  • 应用程序双方是不对等的,服务器要先行启动,处于被动的等待访问的状态,而客户机则可以随时主动地请求访问服务器
  • 服务器进程将套接字绑定到指定的端口,客户端需要知道服务端的网络地址
  • 客户端套接字使用动态分配的自由端口,不需要进行绑定
  • 客户端必须先发送数据报,在数据报中携带双方的地址,服务端收到后才能知道客户机端的地址,才能给客户机端回送数据报
  • 服务器可以接收多个客户端的数据

系统调用:

  • SENDTO():发送数据报,需要一个全相关的五元组信息(协议(UDP)、源IP地址、源端口号、目的IP地址和目的端口号)

    WINSOCK_API_LINKAGE int WSAAPI sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);

    s:发送方的数据报套接字描述符

    buf:发送缓冲区的长度

    flags:发送方式,一般为0

    to:指针,指向的 sockaddr 结构包含接收方完整的地址

    tolen:sockaddr 结构的长度

  • RECVFROM():接收数据报

    WINSOCK_API_LINKAGE int WSAAPI recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);

C/S实例

在面向连接的基础上稍作修改即可

服务端

相比面向连接,这里多了一些异常处理,socket 的参数为 socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;
    WORD sockVersion = MAKEWORD(2, 2);

    SOCKET serSocket;
    SOCKADDR_IN serAddr;

    SOCKADDR_IN remoteAddr;

    int nAddrLen = sizeof(SOCKADDR);

    if (WSAStartup(sockVersion, &wsaData) != 0)
    {
        return 0;
    }

    serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (serSocket == INVALID_SOCKET)
    {
        printf("socket error!");
        return 0;
    }

    serAddr.sin_addr.S_un.S_addr = INADDR_ANY;
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(8888);

    if (bind(serSocket, (SOCKADDR *)&serAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
    {
        printf("bind error!");
        closesocket(serSocket);
        return 0;
    }

    while (1)
    {
        char recvData[255];
        int ret = recvfrom(serSocket, recvData, 255, 0, (SOCKADDR *)&remoteAddr, &nAddrLen);
        if (ret > 0)
        {
            recvData[ret] = 0x00;
            printf("接受到一个连接:%s\r\n", inet_ntoa((remoteAddr.sin_addr)));
            printf((recvData));
        }
        char *sendData = "一个来自服务端的UDP数据包\n";
        sendto(serSocket, sendData, strlen(sendData), 0, (SOCKADDR *)&remoteAddr, nAddrLen);
    }

    closesocket(serSocket);
    WSACleanup();
    return 0;
}

客户端

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;
    WORD sockVersion = MAKEWORD(2, 2);

    SOCKET sockClient;

    if (WSAStartup(sockVersion, &wsaData) != 0)
    {
        return 0;
    }

    sockClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    SOCKADDR_IN sin;

    sin.sin_family = AF_INET;
    sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    sin.sin_port = htons(8888);

    int len = sizeof(sin);

    char *sendData = "来自客户端的数据包.\n";
    sendto(sockClient, sendData, strlen(sendData), 0, (SOCKADDR *)&sin, len);

    char recvData[255];
    int ret = recvfrom(sockClient, recvData, 255, 0, (SOCKADDR *)&sin, &len);
    if (ret > 0)
    {
        recvData[ret] = 0x00;
        printf(recvData);
    }

    closesocket(sockClient);
    WSACleanup();
    return 0;
}

先启动服务端,再启动客户端

image-20250319171111646