你不懂TCP/IP编程,面试官连面试你的机会都不给你

文章目录

  • Socket Related Knowledge
    • Socket在Linux中的表示
    • Socket在Windows中的表示
    • Socket套接字类型
      • 流格式套接字(SOCK_STREAM)
      • 数据报套接字(SOCK_DGRAM)
    • 计算机网络相关知识点
    • Linux系统Socket通信程序Demo
      • 服务端socket程序
      • 客户端socket程序
    • Windows系统Socket通信程序Demo
      • 服务端程序
      • 客户端程序
    • 相关数据结构
    • Windows系统下服务端持续不断地监听客户端的请求
    • Socket缓冲区
      • 阻塞IO
    • 什么是TCP的粘包问题
    • Socket编程实现文件传输功能
      • 服务端程序
      • 客户端程序
    • 网络传输中的大端和小端问题
    • 通过域名获取IP地址
    • 基于`UDP`协议的客户端和服务端实现
      • 服务端程序
      • 客户端程序

Socket Related Knowledge

资源参考自网络

Socket在Linux中的表示

带有ID的文件

  • 0:标准输入文件,对应键盘
  • 1:标准输出文件,对应显示器

一个文件描述符只是一个和打开的文件相关联的整数,背后代表的意思可能如下:

  • 普通文件
  • FIFO
  • 管道
  • 终端
  • 键盘
  • 显示器
  • 一个网络连接

socket()的返回值就是文件描述符

  • read(): 读取远程计算机传来的数据
  • write():向远程计算机写入数据

Socket在Windows中的表示

文件句柄,需要调用专门针对socket而设计的数据传输函数

Socket套接字类型

流格式套接字(SOCK_STREAM)

特征

  • 数据在传输的过程中不会消失(只要SOCK_STREAM不消失,数据就不会消失)
  • 数据按顺序传输
  • 数据的发送和接收不同步

流格式套接字内部有一个缓冲区,通过socket传输的数据将保存在这个缓冲区中。接收端手法哦数据后并不是立即读取,只要数据不超过缓冲区容量,接收端可以在缓冲区被填满之后一次性读取,也可以分多次读取。(接收端可以控制数据的读取)

数据报套接字(SOCK_DGRAM)

无连接的套接字,用SOCK_DGRAM表示

只管传输数据,不做数据校验。

优缺点:传输效率快但是不能保证数据的有效性(数据会丢失)

特点:

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 限制每次传输的数据大小
  • 数据的发送和接收是同步的(也叫:存在数据边界)

QQ视频聊天和语音聊天则使用SOCK_DRRAM来传输数据,首先先保证通信效率,降低延迟,视频和音频即使丢失一小部分数据,也不会对最终数据在终端的显示造成什么大的影响。

计算机网络相关知识点

  • IP地址

  • MAC地址

  • 端口号:

    一台计算机可以同时提供多种网络服务(Web服务,FTP服务,SMTP服务),为区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号,不同计算机上的使用同一个端口通信的计算机可以通过这个端口号进行数据通信

Linux系统Socket通信程序Demo

服务端socket程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){
    
    // 1.配置好需要连接的目标主机的socket信息(IP地址和端口号)
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    // 2.创建套接字(用于打开socket通信流)
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    // 3.将套接字和IP、端口绑定
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    // 4.进行socket监听,等待用户发起请求
    listen(serv_sock, 20);

    // 5.接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

    ///////////  正常情况下:程序运行到accept()就会被阻塞,等待客户端发起请求
    
    
    // 6.向客户端发送数据(通过socket流向远程写数据)
    char str[] = "http://c.biancheng.net/socket/";
    write(clnt_sock, str, sizeof(str));
   
    //关闭套接字
    close(clnt_sock);
    close(serv_sock);

    return 0;
}

客户端socket程序

#include 
#include 
#include 
#include 
#include 
#include 

int main(){
    // 1.配置好客户端需要连接的主机的IP地址和端口号
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    
    // 2.创建套接字,创建socket连接对象
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    // 3.向上面配置好的地址(IP地址+端口号)发起请求
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    // 直到服务器传回数据后,connect() 才运行结束。
    
    // 4.读取服务器传回的数据
    char buffer[40]; // 问题来了,这个buffer的长度是不是足够呢??
    // 很明显:当服务端的数据长度远超过这个Buffer的长度的时候客户端,则需要对服务端的数据进行相应的处理才能保证客户端能够读取到所有的服务端数据
    read(sock, buffer, sizeof(buffer)-1);
   
    // 打印查看服务端数据(当然要保证服务端传过来的数据是可见字符,不然没得看QAQ)
    printf("Message form server: %s\n", buffer);
   
    //关闭套接字
    close(sock);

    return 0;
}

Windows系统Socket通信程序Demo

服务端程序

关于AF_INET与PF_INET的相关介绍: AF_INET与PF_INET

#include 
#include 
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
int main(){
    // 1.初始化 DLL
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    
    // 2.配置好需要连接的主机的IP地址和端口号
    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);  //端口
    
    
    // 3. 创建socket套接字,用于通信
    SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 4. 将socket绑定到上面配置好的地址,用于和这个地址的应用程序进行通信
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    
    // 5.监听
    listen(servSock, 20);
    
    // 6.接收客户端请求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
    
    
    // 7.向客户端发送数据(当然我可以不发送的)
    char *str = "Hello World!";
    send(clntSock, str, strlen(str)+sizeof(char), NULL); //这里是send,Linux中是write
    
    //关闭套接字
    closesocket(clntSock);
    closesocket(servSock);
    //终止 DLL 的使用
    WSACleanup();
    return 0;
}

客户端程序

#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")  //加载 ws2_32.dll
int main(){
    // 1.初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    
    // 2.配置需要连接的远程主机的socket信息
    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);
    
    // 3.创建套接字用于连接远程主机,使用TCP协议进行通信
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
   
    // 4.连接远程主机
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    
    
    // 5.接收服务器传回的数据
    // 单单一个buffer是不足以接收服务器发过来的所有数据的
    char szBuffer[MAXBYTE] = {0};
    recv(sock, szBuffer, MAXBYTE, NULL);  //使用recv函数,Linux使用read函数
    
    
    // 5.输出服务器发过来的数据
    printf("Message form server: %s\n", szBuffer);
    // 6.关闭套接字
    closesocket(sock);
    
    
    // 7.终止使用 DLL
    WSACleanup();
    system("pause");
    return 0;
}

点评:上面每一次连接都要打开关闭连接,有点笨,可以专门做一个工具类来控制socket对象的打开和关闭,仅调用一次,收发数据抽象为功能函数,可以多次调用。可以将Socket资源的打开关闭以RAII的方式进行封装。

相关数据结构

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填充
};
struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};
struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};
struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

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

Windows系统下服务端持续不断地监听客户端的请求

服务端程序在listen监听程序后面加上如下代码:

//接收客户端请求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    char buffer[BUF_SIZE] = {0};  //缓冲区
    while(1){
        SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
        int strLen = recv(clntSock, buffer, BUF_SIZE, 0);  //接收客户端发来的数据
        send(clntSock, buffer, strLen, 0);  //将数据原样返回
        closesocket(clntSock);  //关闭套接字
        memset(buffer, 0, BUF_SIZE);  //重置缓冲区
    }

Socket缓冲区

  • 输入缓冲区
  • 输出缓冲区

write()/send()先将数据写入到缓冲区内部,再根据TCP协议将缓冲区中的数据发送到目标机器,一旦将数据写入缓冲区,函数就返回,不管后面的机器发送数据的过程。

read()/recv()函数从缓冲区读取数据,而不是从网络中读取数据。

缓冲区特性:

  • I/O缓冲区再每个套接字中单独存在
  • I/O缓冲区在创建套接字时自动生成
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据
  • 关闭套接字将丢失输入缓冲区中的数据

获取输入输出黄忠的大小:getsockopt()

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

阻塞IO

使用write()/send()发送数据时

  1. 先检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,则write()/send()会被阻塞,直到缓冲区中的数据被成功发送,缓冲区为空,才唤醒write()/send()继续写入数据
  2. 如果程序在TCP协议下正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send()函数会被阻塞,直到数据发送完毕,才对缓冲区解锁,才将write()/send()唤醒
  3. 如果要写入的数据大于缓冲区的最大长度,则将分批写入
  4. 直到所有的数据被写入缓冲区write()/send()才能返回

当使用read()/recv()读取数据时:

  1. 先检查缓冲区,如果缓冲区中有数据,则读取;否则函数被阻塞,直到网络上有数据来
  2. 如果要读取的数据长度小于缓冲区中的数据长度,则不能一次性将缓冲区中的所有数据读出**,剩余数据将不断积压,直到有 read()/recv()函数再次读取。**
  3. 直到读取到数据后,read()/recv()函数才会返回,否则一直被阻塞

什么是TCP的粘包问题

对于Socket方式的数据发送和接收方式而言,数据的接收和数据发送是无关的,不管数据通过write()/send()发送了多少次,都会尽可能多的发送数据,根据上面的Socket中的内容可以看出,在服务端,只要你缓冲区为空,我就唤醒write()/send()进行数据的发送;而在服务端我则是等待read()/recv()可用的情况才对缓冲区中的数据进行读取,如此便会出现服务端和客户端数据发送/接收数据速率不同步的问题。因此可能会出现数据的**粘包问题。**举例如下:例如客户端向服务器第一次发送 1,第二次发送 3,服务器可能当成 13 来处理。

可以在上面的章节"Windows系统Socket通信程序Demo"让服务器线程在接收客户端数据前等待足够长的一段时间,比如:Sleep(10000);。可以很容易观测到客户端程序多次发送的数据在服务器端形成了"粘包问题"。

Socket编程实现文件传输功能

服务端程序

对于服务端程序,当读取到文件末尾时,fread()函数会返回0,可以在while中判断fread()是否返回0来结束服务端程序接收客户端文件的功能。

#include 
#include 
#include 
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
    //先检查文件是否存在
    char *filename = "D:\\send.avi";  //文件名
    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;
}

客户端程序

对于客户端程序,通过判断recv()函数是否返回0来结束while循环,表示文件接收完成。

recv()==0的出现时机:当数据传输完成,客户端会受到服务器传来的FIN包,由此客户端就知道服务器不会再向自己传输数据,此时recv()正好等于0。

关于FIN的介绍,可以查看谢希仁的计算机网络的TCP的握手关闭连接那一章节。

在服务端,可以显式调用shutdown()函数来发送FIN包,该方式会等待服务器端输出缓冲区中的数据传输完毕,而服务器端直接调用close()/closesocket()函数则会使得输出缓冲区中的数据失效,因此丢失了数据。

#include 
#include 
#include 
#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;
}

网络传输中的大端和小端问题

大小端问题本质上是CPU中的数据存储方式问题

以4字节int类型数据0x12345678,存储该数据的内存首地址为0x20来说

  • 大端序:高位字节存放到地位地址,上述数据的存放则为

    • 0x20: 0x12
    • 0x21: 0x34
    • 0x22: 0x56
    • 0x23: 0x78

    其中:表示“存放”的意思

  • 小端序:高位字节存到放高位地址上,上述数据的存放为

    • 0x20: 0x78
    • 0x21: 0x56
    • 0x22: 0x34
    • 0x23: 0x12

默认顺序:主流Intel系列CPU为小端序,网络字节序为大端序。

所以对于收发数据的主机来说需要做如下处理:主机 A 先把数据转换成大端序再进行网络传输,主机 B 收到数据后先转换为自己的格式再解析。

通过域名获取IP地址

通过该函数即可:**struct** hostent *gethostbyname(**const** char *hostname);

相关数据解构:

struct hostent{
    char *h_name;  //official hostname
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}

关于h_aliasesh_addr_list的相关介绍:

  • h_aliases: char**
    • alias #1: char*
    • alisa #2: char*
    • NULL
  • h_addr_list:char**
    • IP addr #1: char*
    • IP addr #2:char*`
    • IP addr #3:char*`
    • NULL

代码示例:

#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")
int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    struct hostent *host = gethostbyname("www.baidu.com");
    if(!host){
        puts("Get IP address error!");
        system("pause");
        exit(0);
    }
    //别名
    for(int i=0; host->h_aliases[i]; i++){
        printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
    }
    //地址类型
    printf("Address type: %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
    //IP地址
    for(int i=0; host->h_addr_list[i]; i++){
        printf("IP addr %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
    }
    system("pause");
    return 0;
}

运行结果:

Aliases 1: www.baidu.com
Address type: AF_INET
IP addr 1: 61.135.169.121
IP addr 2: 61.135.169.125

基于UDP协议的客户端和服务端实现

UDP:用户数据报协议,协议的具体介绍在谢希仁的计算机网络中有介绍,更具体的可以查询RFC文档来了解该协议的具体内容信息。使用UDP协议进行通信的双方不需要建立连接。

发送数据:

ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);  //Linux
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen);  //Windows

参数介绍:

  • sock:用于传输 UDP数据的套接字
  • buf:保存待传输数据的缓冲区地址
  • nbytes:带传输数据的长度(以字节计)
  • flags:可选项参数,若没有可传递 0
  • to:存有目标地址信息的sockaddr 结构体变量的地址
  • addrlen:传递给参数 to 的地址值结构体变量的长度

接收数据:

ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);  //Linux
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen);  //Windows

参数介绍:

  • sock:用于接收 UDP 数据的套接字
  • buf:保存接收数据的缓冲区地址
  • nbytes:可接收的最大字节数(不能超过 buf缓冲区的大小)
  • flags:可选项参数,若没有可传递 0
  • from:存有发送端地址信息的 sockaddr结构体变量的地址
  • addrlen:保存参数 from 的结构体变量长度的变量地址值

服务端程序

Windows平台:

#include 
#include 
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    //创建套接字
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
    //绑定套接字
    sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));  //每个字节都用0填充
    servAddr.sin_family = PF_INET;  //使用IPv4地址
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
    servAddr.sin_port = htons(1234);  //端口
    bind(sock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR));
    //接收客户端请求
    SOCKADDR clntAddr;  //客户端地址信息
    int nSize = sizeof(SOCKADDR);
    char buffer[BUF_SIZE];  //缓冲区
    while(1){
        int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);
        sendto(sock, buffer, strLen, 0, &clntAddr, nSize);
    }
    closesocket(sock);
    WSACleanup();
    return 0;
}

客户端程序

#include 
#include 
#pragma comment(lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    //创建套接字
    SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
    //服务器地址信息
    sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));  //每个字节都用0填充
    servAddr.sin_family = PF_INET;
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servAddr.sin_port = htons(1234);
    //不断获取用户输入并发送给服务器,然后接受服务器数据
    sockaddr fromAddr;
    int addrLen = sizeof(fromAddr);
    while(1){
        char buffer[BUF_SIZE] = {0};
        printf("Input a string: ");
        gets(buffer);
        sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&servAddr, sizeof(servAddr));
        int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);
        buffer[strLen] = 0;
        printf("Message form server: %s\n", buffer);
    }
    closesocket(sock);
    WSACleanup();
    return 0;
}

你可能感兴趣的:(C++服务器编程,C++基础)