TCP/UDP 套接字的编写

文章目录

基础知识

  • IP地址互联网协议地址(Internet Protocol Address),分配给互联网互联中设备的单一标识 ,理解成生活中的邮箱地址是比较类似的。在不同的地方入网,就会有不同的ip地址
  • 端口号(port) :没操作系统的知识的话,说的粗陋一点,就是如何识别在体态设备上与不同的APP通信,给这些APP分配一个ID,这个ID就叫做端口号。当然前面说法并不完全正确,准确的是端口号应该是区别一台主机中该网络信息向上交付时,是应该交由哪个进程处理而端口号就是用来做这个的 。和进程PID有点类似但又有所不同。
    • 一个进程可以有多个port,因为其可以同时收到多个不同的网络消息。
    • 一个port只能对应一个进程,因为他就是用来标识信息向上交付时交给哪个进程的
    • 端口号的范围从0到65535,其中一些知名的端口(0-1023)被预留给标准服务,比如HTTP使用80端口,HTTPS使用443端口,FTP使用21端口等。而动态或私有端口(通常指从1024到65535)则通常是临时分配给应用程序的,尤其是在客户端或不固定服务场景中
  • 网络字节序 :就是大小端存储的问题,网络中发送的数据需要考虑大小端,网络规定的标准是大端
    • Big-endian:大端,指一个数据的低字节部分存在高地址处
    • Little-endian:小端,。指一个数据他的低字节处 存储在低地址处。
    • 例子:一个2字节数据:0x11 22 ,加入从左向右地址是增长的,那么大端里面:0x11 22 ;小端:0x22 11
    • 这里说一下我自己容易搞错的点,数据的右边一般才是低字节序,数据的左边一般是高字节序列。你想一个整数,百位肯定是高位把。
    • linux/windows系统都有提供的从主机转网络,网络转主机的函数
    • htonl
    • htons
    • ntohl
    • ntohs

socket编程

socket编程虽然里面需要填写的字段十分多,但是大多数场景下基本类似。

先来整体认识下一些必须要掌握的基础知识

常用接口(windows和linux的函数名都是一样的,但是由于系统不同,其中有些参数类型可能不太一样,以下以WIndows为例 ,linux你可以使用man + 函数名 查看具体情况)

这些常用接口除了类型有一些略微不同,其中许多宏常量,linux和Windows下几乎都是一样的。下面介绍的函数名windows和linux基本都共有。

  • socket
cpp 复制代码
  SOCKET WSAAPI socket(
    [in] int af,   // 常常传入 AF_INET 或者AF_INET6
    [in] int type,  
    [in] int protocol
    );
    • [in]是windows文档的一种习惯,表示这个参数是一个输入形参数
    • af是协议家族, 常常传入 AF_INET 或者AF_INET6。你是ipv4地址就传入AF_INET,ipv6就选AF_INET6
    • type 重点了解,UDP传入SOCK_DGRAM,TCP传入SOCK_STREAM。其余选项可自行取查看手册
    • protocol:如果af,type指定了ipv4/6的协议家族和对应的类型,那么这里传入0由系统给你自动匹配协议是最好。的。如果你传入的是类似SOCK_RAW的值,那这里就需要你特殊指定,这个选项是用来精确控制套接字收到的数据。
  • 返回值

    如果未发生错误, 套接字 将返回引用新套接字的描述符。 否则,将返回值 INVALID_SOCKET,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。

  • bind :一般服务端使用

cpp 复制代码
* int bind(
  [in] SOCKET         s,
       const sockaddr *addr,
  [in] int     namelen
);
  • 参数

    • [in] s
      标识未绑定套接字的描述符。
    • addr
      指向要分配给绑定套接字 的本地地址 的 sockaddr 结构的指针。
    • [in] namelen
      addr 指向的值的长度(以字节为单位)。
  • 返回值

  • 如果未发生错误, 绑定 将返回零。 否则,它将返回SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。

谈到bind,就必须提及Socket编程里面需要的用的结构体sockaddr ,sockaddr_in,sockaddr_in6

  • 使用bind时,一般先创建都应的sockaddr_in(ipv4),sockaddr_in6(ipv6)的。填写其中的家族,端口号,和ip地址。
  • 在强转成 sockaddr* 传入对应的函数bind处理即可。
  • 其中要注意主机大小端和ip地址格式问题。用上述的网络字节序处的函数转变端口号
  • ip地址和和主机之间相互转换的时候,库里面的函数
    如下:

int inet_aton(const char *cp, struct in_addr *inp);

char *inet_ntoa(struct in_addr in);

in_addr_t inet_addr(const char *cp);
inet_ntop, inet_pton:这两个是都支持的

下面几个用的相对较少,可以使用时查文档

ntohf

ntohl

ntohll

ntohs

cpp 复制代码
struct sockaddr {
        ushort  sa_family;
        char    sa_data[14];
};

struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

struct sockaddr_in6 {
        short   sin6_family;
        u_short sin6_port;
        u_long  sin6_flowinfo;
        struct  in6_addr sin6_addr;
        u_long  sin6_scope_id;
};

上面两个操作TCP和UDP都会用到,但是下面介绍的就是TCP才用了

  • listen :一般是TCP服务端代码使用
cpp 复制代码
int WSAAPI listen(
  [in] SOCKET s,
  [in] int    backlog
);
  • 参数解释

    • [in] s 标识绑定的未连接的套接字的描述符。
    • [in] backlog 挂起的连接队列的最大长度。
  • 返回值:如果未发生错误, 则侦听 返回零。 否则,将返回 值 SOCKET_ERROR ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。

  • accept :一般时TCP服务器端使用

cpp 复制代码
SOCKET WSAAPI accept(
  [in]      SOCKET   s,
  [out]     sockaddr *addr,
  [in, out] int      *addrlen
);
  • 参数

    [in] s :一个描述符,用于标识已使用 侦 听函数置于侦听状态的套接字。 连接实际上是使用 accept 返回的套接字建立的。

    [out] :addr指向接收连接实体地址的缓冲区的可选指针,该地址称为通信层。 addr 参数的确切格式由创建 sockaddr 结构中的套接字时建立的地址系列确定。

    [in, out] :addrlen指向包含 addr 参数指向的结构长度的整数的可选指针。

  • 返回值

    果未发生错误, 则 accept 将返回 类型为 SOCKET 的值,该值是新套接字的描述符。 此返回值是建立实际连接的套接字的句柄。

    否则,将返回 值 INVALID_SOCKET ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。

这里返回值拿到的套接字正常读取写入就能和客户端通信了,当成文件描述符使用即可,当然用send和recv来收发数据也可以。

  • connect :一般TCP客户端代码使用
    • 参数

      [in] s 标识未连接的套接字的描述符。

      [in] name 指向应建立连接的 sockaddr 结构的指针。

      [in] namelen name 参数指向的 sockaddr 结构的长度(以字节为单位)。

    • 返回值

      如果未发生错误, 则连接 返回零。 否则,它将返回SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。

链接成功后就可以使用,当成文件描述符来使用也是没有问题的。当然用send和recv来收发数据也可以

UDP套接字编程

UDP套接字就只需要两个操作,socket和bind,做完之后UDP程序就能通信了。
具体步骤
服务器

  • 先申请一个套接字,填入对应的sockaddr的字段
  • 再bind 改套接字即可
  • 再用sendto 和 recv收发数据

客户端:

  • 创建一个套接字
  • 用sendto 和recv发收数据即可
    Server
cpp 复制代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>

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

int main() {
    //使用windows网络库需要做的,linux下就没这过程
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "Failed to initialize winsock." << std::endl;
        return -1;
    }

    SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (serverSocket == INVALID_SOCKET) {
        std::cerr << "Failed to create socket." << std::endl;
        WSACleanup();
        return -1;
    }

    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(12345); // 服务器监听端口
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
        std::cerr << "Failed to bind socket." << std::endl;
        closesocket(serverSocket);
        WSACleanup();
        return -1;
    }

    std::cout << "Server is listening on port 12345..." << std::endl;

    char buffer[1024];
    sockaddr_in clientAddr;
    int addrLen = sizeof(clientAddr);

    while (true) {
        int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (SOCKADDR*)&clientAddr, &addrLen);
        if (bytesReceived > 0) {
            buffer[bytesReceived] = '\0';
            std::cout << "Message from client: " << buffer << std::endl;

            // Echo back the message to the client
            sendto(serverSocket, buffer, bytesReceived, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr));
        }
    }

    closesocket(serverSocket);
    WSACleanup();

    return 0;
}

Client

cpp 复制代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>

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

int main() {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "Failed to initialize winsock." << std::endl;
        return -1;
    }

    SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (clientSocket == INVALID_SOCKET) {
        std::cerr << "Failed to create socket." << std::endl;
        WSACleanup();
        return -1;
    }

    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(12345); // 服务器端口号
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); // 服务器IP地址,这里假设为本地环回地址

    std::string message;
    std::cout << "Enter a message to send to the server: ";
    std::getline(std::cin, message);

    if (sendto(clientSocket, message.c_str(), message.length(), 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
        std::cerr << "Failed to send message." << std::endl;
    } else {
        std::cout << "Message sent to server." << std::endl;
    }

    char buffer[1024];
    int bytesReceived = recvfrom(clientSocket, buffer, sizeof(buffer), 0, NULL, NULL);
    if (bytesReceived > 0) {
        buffer[bytesReceived] = '\0';
        std::cout << "Reply from server: " << buffer << std::endl;
    }

    closesocket(clientSocket);
    WSACleanup();

    return 0;
}

TCP套接字编写

服务端:

  • socket获取一个套接字
  • bind这个套接字
  • 监听这个套接字
  • accept,接受链接成功返回一个套接字,用该套接字通信即可。用send和recv进行通信即可。可以接受多个套接字

客户端:

  • socket 获取一个套接字
  • connect 对应的服务器,成功后用该套接字 send和recv即可。

linux下一切皆文件,TCP这里读取内容的时候,其实用read和write来收发数据也是没有问题的。

服务器代码

cpp 复制代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

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

int main() {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("WSAStartup failed: %d\n", GetLastError());
        return 1;
    }

    SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) {
        printf("Error at socket(): %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr("127.0.0.1"); // 或者使用INADDR_ANY监听所有地址
    service.sin_port = htons(27015); // 自定义端口号

    if (bind(ListenSocket, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
        printf("listen failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    printf("Listening on port 27015...\n");

    SOCKET ClientSocket;
    while ((ClientSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET) {
        char recvbuf[512];
        int iResult = recv(ClientSocket, recvbuf, 512, 0);
        if (iResult > 0)
            printf("Received: %s\n", recvbuf);
        else if (iResult == 0)
            printf("Connection closing...\n");
        else
            printf("recv failed: %d\n", WSAGetLastError());

        // 发送响应(可选)
        char *response = "Hello from server!";
        send(ClientSocket, response, strlen(response), 0);

        closesocket(ClientSocket);
    }

    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    closesocket(ListenSocket);
    WSACleanup();

    return 0;
}
cpp 复制代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

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

int main() {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("WSAStartup failed: %d\n", GetLastError());
        return 1;
    }

    SOCKET ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ConnectSocket == INVALID_SOCKET) {
        printf("Error at socket(): %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址
    serverAddress.sin_port = htons(27015); // 与服务器端相同的端口号

    if (connect(ConnectSocket, (SOCKADDR*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
        printf("connect failed: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    char sendbuf[] = "Hello from client!";
    int iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    char recvbuf[512];
    iResult = recv(ConnectSocket, recvbuf, 512, 0);
    if (iResult > 0)
        printf("Received: %s\n", recvbuf);
    else if (iResult == 0)
        printf("Connection closed\n");
    else
        printf("recv failed: %d\n", WSAGetLastError());

    closesocket(ConnectSocket);
    WSACleanup();

    return 0;
}

套接字编写注意事项

  • TCP和UDP都是全双工的,不用担心收发数据互相干扰的问题
  • 为了收发不互相干扰,一般要采取并发
相关推荐
C++忠实粉丝18 分钟前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
九州ip动态18 分钟前
做网络推广及游戏注册为什么要换IP
网络·tcp/ip·游戏
Estar.Lee23 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
陌小呆^O^1 小时前
Cmakelist.txt之win-c-udp-client
c语言·开发语言·udp
日晨难再2 小时前
嵌入式:STM32的启动(Startup)文件解析
stm32·单片机·嵌入式硬件
速盾cdn8 小时前
速盾:CDN是否支持屏蔽IP?
网络·网络协议·tcp/ip
yaoxin5211238 小时前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin8 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
PegasusYu12 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组