• TCP套接字编程详解


    目录

    为什么socket编程又叫套接字编程?

    TCP服务端

    初始化套接字库——WSAStarup

    创建套接字——socket

    绑定到本机 ——bind

    开始监听 ——listen

    连接客户端请求——accept

    发送与接收数据 ——send/recv

    完整代码

    TCP客户端 

    初始化套接字库——WSAStarup 

    创建套接字 

    连接服务器——connect

    发送与接收数据 

    完整代码 


    为什么socket编程又叫套接字编程?

     为什么要称socket为套接字?首先套接字的原词为"socket",直译过来就是插座的意思,最先采用这个词的人,觉得网络连接,就像插口和插座一样,一方插,一方被插(知乎用户回答)

        除此之外,linux等系统中“套接字”对应“socket word”,所以“字”也就是对应“word”,可能指计算机数据,也可能指存储socket的数据表示,因为端口号是两个字节,就是一个WORD。

        至于为什么翻译为“套接字”:有人说是“套用-接口-标识”的意思;有人说是“套接起来的字符串”的意思;有人说“是将网络数据包一层一层地套起来传输”的意思。

        总之就是望文生义,毕竟怎么解释都不重要,东西还是那个东西

        看了一圈,最比较赞同的解释是:套接指的是套接管,就是奖两根水管套接起来的关资,然后“字”就是连接数据的标识符,所以套接字就是标识连接的数据体。(附作者链接Socket为什么要翻译成套接字? - FrankIsFree的回答 - 知乎

    TCP服务端

    首先创建一个空的控制台应用,按照TCP实现流程编写代码。 

    初始化套接字库——WSAStarup

    1. //初始化套接字库
    2. WORD wVersion;
    3. WSADATA wsaData;
    4. int err;
    5. wVersion = MAKEWORD(1, 1); //MAKEWORD(a,b) b|a<<8
    6. err = WSAStartup(wVersion, &wsaData);
    7. //检查1
    8. if (err != 0) {
    9. return err;
    10. }
    11. //检查2
    12. if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
    13. //清理套接字库
    14. WSACleanup();
    15. return -1;
    16. }

    当我们再进行socket编程时,要调用各种socket函数,而且还需要用到一个库文件Ws2_32.lib个一个头文件Winsock2.h 

    wsastartup()函数向操作系统说明,我们要用哪个库文件。 因此就可以将库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数了。 一句话解释:wsastartup()主要就是进行相应的socket库绑定。 

    WSAStarup

    • W:windows
    • S:socket
    • A:Asynchronous异步
    • Starup:启动
    int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData )

    WSAStartup需要传入两个参数,wVersionRequested指明需要库的版本号,lpWSAData为指向WSAData数据结构的指针,用来接收Windows Sockets实现细节(存储初始化数据)

    调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0。  否则返回对应错误的宏。

    WORD是微软SDK中的类型,意为'字' ,是2byte无符号整数,表示范围0~65535相当于C语言中的2个char

    MAKEWORD();可以创建WORD类型。其工作原理类似于bLow | bHigh<<8

    1. WORD MAKEWORD( //函数原型
    2. BYTE bLow, //指定新变量的低字节序;
    3. BYTE bHigh //指定新变量的高字节序;
    4. );

    在进行初始化结果判断时需判断两个:第一个是WSAStarup函数的返回值,判断返回结果是否为0;第二个是存储初始化数据WSAData的高位和地位是否和我们指定的库版本一致

    1. //检查1
    2. if (err != 0) {
    3. return err;
    4. }
    5. //检查2
    6. if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
    7. //清理套接字库
    8. WSACleanup();
    9. return -1;
    10. }
    • WSACleanup()函数 

            int WSACleanup (void); 

            应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源

    创建套接字——socket

    1. //创建套接字
    2. SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

    socket函数用于创建一个socket描述符,它唯一标识一个socket,包含了三个参数:

    int socket(int domain, int type, int protocol); 

    • domain:协议域/协议族

    协议族决定了socket的地址类型,在通信中必须采用相应的地址.

    • type:socket通信类型 

    函数socket()的参数type用于设置socket通信类型


    并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。 

    类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。一旦连接,可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内任然没有接受完毕,可以将这个连接人为已经死掉。
    SOCK_DGRAM和SOCK_RAW 这个两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接受数据,recvfrom()接受来自制定IP地址的发送方的数据。
    SOCK_PACKET是一种专用的数据包,它直接从设备驱动接受数据。 

    • protocol:制定某个协议的特定类型

    函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

    绑定到本机 ——bind

    1.首先准本绑定信息。

    在准本绑定信息中既要指明绑定的IP地址,同时也要指明绑定的端口号

    1. SOCKADDR_IN addrSrv;
    2. //地址
    3. addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    4. //协议族,与上面保持一致
    5. addrSrv.sin_family = AF_INET;
    6. //端口;0~65535,其中1024以下的端口为系统保留的
    7. addrSrv.sin_port = htons(6000);

    sockaddr 和 sockaddr_in 这两个结构体用来处理网络通信的地址。 

    sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了;sockaddr_in该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中 

    sockaddrsockaddr_in二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

        sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。 
        sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
     

    htonl函数

    主机的unsigned long值转换成网络字节顺序。主要针对32位的(long)

    htons函数

    htons()作用是将端口号由主机字节序转换为网络字节序的整数值。主要针对16位的(short)

    htonl和htons函数

    • h:host主机
    • to:转换
    • n:network:网络
    • l:32位l的ong
    • s:16位的short

    之所以要进行字节序的转换,是因为主机字节序和网络字节序的存储不同

    主机字节序
    1)大端存储:低位字节放在内存的高地址端,高位字节放在内存的低地址端
    2)小端存储:低位字节放在内存的低地址端,高位字节放在内存的高地址端

    网络字节序:
    UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端存储而一般x86计算机用的是小端存储~

         在一些socket通信的服务器程序中我们会看到在服务器bind IP地址和端口号时,我们不是bind明确的IP地址(如222.20.79.150),而是使用INADDR_ANY。

       INARRD_ANY是用于多网卡的机器上的,多网卡就会有多个IP地址。比如你的机器有3个IP:192.168.1.1、202.202.202.202和61.1.2.3。如果设置serv.sin_addr.s_addr=inet_addr("192.168.1.1");然后监听100端口,这时其他机器只有连接到192.168.1.1才能成功;连接202.202.202.202:100或61.1.2.3:100都会失败。如果设置serv.sin_addr.s_addr=htonl(INADDR_ANY);的话,无论连接哪个IP都可以连接上。

        总的来说INADDR_ANY参数就表明可以连接到本机的所有ip都是可以的,极大的简化了需要创建socket的数量,因为我们就绑定一个INADDR_ANY和一个端口,然后客户端通信到这个机器的所有ip都用这个socket来处理。

    2.绑定信息准备好后则进行绑定到本机

    1. //绑定
    2. bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

    bind()函数原型

    int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen);

    1.sockfd:即为socket描述字,他是通过socket()函数创建的,唯一标识一个socket。bind函数就是将这个描述子绑定一个名字

    2.addr:一个sockaddr*指针,指向地址结构的指针,根据创建socket时的地址协议不同而不同。

    3.addrlen:对应地址结构的长度。通常服务器在启动时会绑定一个众所周知的地址(ip地址+端口号)。而客户端不用指定系统自动分配,所以通常服务端在listen之前要调用bind(),而客户端不会调用,在connect()时由系统随机生成一个。

    bind函数运行成功后会将端口和IP地址绑定到Socket描述符上,返回0时表示绑定成功。

    开始监听 ——listen

    1. //监听
    2. listen(sockSrv, 10);

    listen()函数原型

    int listen(int sockfd, int backlog)

    listen函数的第一个参数时即将要监听的socket描述字,第二个参数为相应的socket可以排队的最大连接数。socket()创建的socket默认是一个主动类型,listen则将socket变成被动类型,等待客户连接请求。

    对于调用listen进行监听的套接字,操作系统会为其维护2个队列:未完成队列,已完成队列。

    • 当客户端发送TCP三次握手的第一次,服务器会在未完成队列创建一个对应项(之后就是等待TCP的第二次和第三次完成)
    • 当三次握手完成后,该连接就会放在完成队列中,接着就可以执行accept函数从队列中移出

    listen的第二参数backlog则表示,这两个队列的和不能超过baklog。

    这个过程就如下图所示:

    连接客户端请求——accept

    1. //接收请求前的准备工作
    2. SOCKADDR addrCli;
    3. int len = sizeof(SOCKADDR);
    4. while (true) {
    5. //接收链接请求,返回针客户端的套接字
    6. SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);
    7. //关闭连接
    8. closesocket(sockConn);
    9. }

    accept()函数 

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • sockfd用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字)
    • addr用来保存客户端套协议地址(包括客户端IP和端口信息等)
    • addrlen是客户端套接字的长度

    返回客户端套接字的标识,一个客户端的socket

    注意:如果没有客户端套接字去请求,它便会在那里一直等下去。如果是非阻塞式的socket, 那么accept函数会立即返回。

    发送与接收数据 ——send/recv

    1. char recvBuf[100];
    2. char sendBuf[100];
    3. while (true) {
    4. //接收连接请求,返回针对客户端的套接字
    5. SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);
    6. //准备发送的数据
    7. sprintf_s(sendBuf, 100, "hello world");
    8. //发送数据
    9. send(sockConn, sendBuf, sizeof(sendBuf) + 1, 0);
    10. //接收数据
    11. recv(sockConn, recvBuf, 100, 0);
    12. std::cout << recvBuf << std::endl;
    13. //关闭套接字
    14. closesocket(sockConn);
    15. }
    1. int send( SOCKET s, const char FAR *buf,  int len, int flags )
    2. int recv( SOCKET s, char FAR *buf,  int len,  int flags );   
    • s指定客户端socket的描述符
    • buf为发送/接收数据的缓冲区
    • len为实际发送数据/接收数据的缓冲区大小
    • flags一般置位0

    完整代码

    1. #include
    2. #include//第二版本的网络库
    3. #pragma comment(lib,"ws2_32.lib")
    4. //一般模板
    5. /*
    6. * 0.初始化套接字库
    7. * 1.创建socket
    8. * 2.绑定到本机
    9. * 3.开始监听
    10. * while(true){
    11. * 4.接收客户端连接
    12. *
    13. * 5.关闭客户端socket
    14. * }
    15. *
    16. * 6.关闭服socket
    17. * 7.清理套接字库
    18. */
    19. int main() {
    20. //初始化套接字库
    21. WORD wVersion;
    22. WSADATA wsaData;
    23. int err;
    24. wVersion = MAKEWORD(1, 1); //MAKEWORD(a,b) b|a<<8
    25. err = WSAStartup(wVersion, &wsaData);
    26. //检查1
    27. if (err != 0) {
    28. return err;
    29. }
    30. //检查2
    31. if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
    32. //清理套接字库
    33. WSACleanup();
    34. return -1;
    35. }
    36. //创建tcp套接字
    37. SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
    38. //绑定到本机
    39. //绑定即要指明绑定的哪个IP地址,同时指明绑定的端口号
    40. //准备绑定信息
    41. SOCKADDR_IN addrSrv;
    42. addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    43. //协议族,与上面保持一致
    44. addrSrv.sin_family = AF_INET;
    45. //端口;0~65535,其中1024以下的端口为系统保留的
    46. addrSrv.sin_port = htons(6000);
    47. //绑定
    48. bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
    49. //监听
    50. listen(sockSrv, 10);
    51. std::cout << "Server start at 6000" << std::endl;
    52. //接收请求前的准备工作
    53. SOCKADDR addrCli;
    54. int len = sizeof(SOCKADDR);
    55. char recvBuf[100];
    56. char sendBuf[100];
    57. while (true) {
    58. //接收链接请求,返回针对客户端的套接字
    59. SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);
    60. //准备发送的数据
    61. sprintf_s(sendBuf, 100, "hello world");
    62. //发送数据
    63. send(sockConn, sendBuf, sizeof(sendBuf) + 1, 0);
    64. //接收数据
    65. recv(sockConn, recvBuf, 100, 0);
    66. std::cout << recvBuf << std::endl;
    67. //关闭套接字
    68. closesocket(sockConn);
    69. }
    70. //关闭套接字
    71. closesocket(sockSrv);
    72. //清理套接字库
    73. WSACleanup();
    74. system("pause");
    75. }

    TCP客户端 

    首先在同一解决方案下另建一个控制台应用,接着按照TCP客户端的实现流程编写代码

    初始化套接字库——WSAStarup 

    客户端初始化套接字库的代码与服务端的基本上是一致的,要注意客户端和服务端的套接字库版本相一致。

    1. //初始化套接字库
    2. WORD wVersion;
    3. WSADATA wsaData;
    4. int err;
    5. wVersion = MAKEWORD(1, 1); //MAKEWORD(a,b) b|a<<8
    6. err = WSAStartup(wVersion, &wsaData);
    7. //检查1
    8. if (err != 0) {
    9. return err;
    10. }
    11. //检查2
    12. if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
    13. //清理套接字库
    14. WSACleanup();
    15. return -1;
    16. }

    创建套接字 

    1. //创建tcp套接字,与服务器保持一致
    2. SOCKET sockCli= socket(AF_INET, SOCK_STREAM, 0);
    3. //绑定到本机
    4. //绑定即要指明绑定的哪个IP地址,同时指明绑定的端口号
    5. //准备绑定信息
    6. SOCKADDR_IN addrSrv;
    7. addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    8. //协议族,与上面保持一致
    9. addrSrv.sin_family = AF_INET;
    10. //端口;0~65535,其中1024以下的端口为系统保留的
    11. addrSrv.sin_port = htons(6000);

    客户端创建套接字要指明绑定的IP地址和端口。

    iner_addr会检查传入的字符串是否是一个合法的IP地址(例如格式或者每个字段是否<255) ,如果非法则返回INADDR_NONE正确执行将返回一个无符号长整数型数

    连接服务器——connect

    connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

    connect()用于建立与指定socket的连接。

    函数原型

    int connect(SOCKET s, const struct sockaddr * name, int namelen);
    • s:标识一个未连接socket(客户端Socket
    • name:指向要连接套接字sockaddr结构体的指针
    • namelen:sockaddr结构体的字节长度

    connect操作之后代表对应的套接字已连接 ,发送与接收数据时就可以使用这个套接字。

    发送与接收数据 

    1. //发送的数据
    2. char sendBuf[] = "world";
    3. //接收的数据
    4. char recvBuf[100];
    5. //发送数据到服务器
    6. send(sockCli, sendBuf, strlen(sendBuf) + 1, 0);
    7. //接收数据到服务器
    8. recv(sockCli, recvBuf, sizeof(recvBuf), 0);

    注意程序结束前要关闭套接字,清理套接字库。

    1. //关闭套接字
    2. closesocket(sockCli);
    3. WSACleanup();

    完整代码 

    1. #include
    2. #include//第二版本的网络库
    3. #pragma comment(lib,"ws2_32.lib")
    4. #include
    5. int main() {
    6. //初始化套接字库
    7. WORD wVersion;
    8. WSADATA wsaData;
    9. int err;
    10. wVersion = MAKEWORD(1, 1); //MAKEWORD(a,b) b|a<<8
    11. err = WSAStartup(wVersion, &wsaData);
    12. //检查1
    13. if (err != 0) {
    14. return err;
    15. }
    16. //检查2
    17. if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
    18. //清理套接字库
    19. WSACleanup();
    20. return -1;
    21. }
    22. //创建tcp套接字,与服务器保持一致
    23. SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
    24. //准备连接信息
    25. //指明要连接的IP地址和端口号
    26. SOCKADDR_IN addrSrv;
    27. addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    28. //协议族,与上面保持一致
    29. addrSrv.sin_family = AF_INET;
    30. //端口;0~65535,其中1024以下的端口为系统保留的
    31. addrSrv.sin_port = htons(6000);
    32. //连接服务器
    33. connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
    34. //发送的数据
    35. char sendBuf[] = "world";
    36. //接收的数据
    37. char recvBuf[100];
    38. //发送数据到服务器
    39. send(sockCli, sendBuf, strlen(sendBuf) + 1, 0);
    40. //接收数据到服务器
    41. recv(sockCli, recvBuf, sizeof(recvBuf), 0);
    42. std::cout << recvBuf << std::endl;
    43. //关闭套接字
    44. closesocket(sockCli);
    45. WSACleanup();
    46. system("pause");
    47. return 0;
    48. }

  • 相关阅读:
    主键、外键、建表范式、MySQL索引、用户管理
    微信怎么发状态?简单教程,一学就会!
    【PCBA方案】快速测温体温计方案
    plyr音视频播放插件使用示例
    《KAN》论文笔记
    牛皮了,Alibaba专家甩出的MySQL笔记,看完我蒙了
    QT之 给控件添加右键菜单
    IP代理是什么?有什么好处?
    学校教务管理系统的设计与实现--Word文档可在最后链接处下载,无code
    【Pytorch】Visualization of Fature Maps(2)
  • 原文地址:https://blog.csdn.net/qq_54169998/article/details/127433371