当前位置: 首页 > article >正文

Socket编程(TCP/UDP详解)

前言:之前因为做项目和找实习没得空,计算机网络模块并没有写成博客,最近得闲了,把计算机网络模块博客补上。

目录

一,UDP编程

1)创建套接字

2)绑定端口号

3)发送与接收数据

4)UDP简单的发送数据和接收数据服务器

二,TCP编程

1)创建套接字

2)绑定端口号

3)使套接字进入监听状态

4)获取成功建立连接的的文件描述符和主机信息

5)发送与接收数据

6)连接其他主机

7)TCP简单的发送数据和接收数据服务器


scoket编程即套接字编程,是网络编程的基础,它允许两台或者多台计算机进行网络通信,这篇文章主要讲socket编程利用里面的TCP和UDP相关接口实现网络通信。

一,UDP编程

在udp编程里面,我们首先要创建一个套接字,也就是文件描述符。用来接收数据与发送数据,但注意,UDP为每一个套接字维护一个缓冲区,但是发送缓冲区是临时的、不可见的。这是为什么呢?UDP是面向无连接的,每次发送数据都是相对独立的,这允许我们可以使用临时的缓冲区,UCP数据发送完就不管任何事了,不会像TCP一样要确认对方收到,没收到还要进行重传等操作。不维护一个长久的缓冲区,也可以节省空间资源,使UDP变得轻量与高效。如果接收缓冲区设置成临时的那么数据到达后,如果应用程序没有及时读取可能出现丢失,那么如果一直等到读取完再销毁,一个套接字缓冲区可能接收很多主机的信息,可能接收缓冲区会频繁的创建,销毁,这会有很多不必要的开销。

1)创建套接字

第一个参数是网络通信协议,如IPV4或者IPV6等,具体参考下图

第二个参数是套接字的类型,使用什么方式通信,如数据报(UDP)或者字节流(TCP)等

返回值为-1代表创建失败,并设置错误码,大于0代表成功创建。

使用例子:

        //AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
        int fd=socket(AF_INET,SOCK_DGRAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
        }

2)绑定端口号

在我的上一篇文章,我们以及明白绑定端口号加上IP才能确定互联网内的唯一一台主机,客户端可以不绑定端口号,这样子操作系统就会随机分配端口号,但是服务端不能这样,不然其他人无法主动连接服务端,因为其他人根本无法发现它,需要被别人第一次主动发现需要绑定端口号。现在我们来学习绑定端口号的接口。

scokfd就是我们前面使用socket接口创建的文件描述符。我们重点介绍接下来第二个参数,第三个参数是第二个参数的长度。

addr是结构体强转后得到的,它可以由IPV4结构体格式强转得到,也可以由IPV6格式强转得到,socketaddr_in是IPV4协议,socketaddr_un是IPV6协议。可以看下图理解

struct socketaddr里面的内容

struct socketaddr_in里面的内容

上图struct in_addr里面的内容

具体初始化和使用例子:

//IPV4结构体
        struct sockaddr_in _addr; 
        //设置为IPV4协议
        _addr.sin_family=AF_INET;
        //端口号网络字节序
        _addr.sin_port=htons(PORT);
        //IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
        _addr.sin_addr.s_addr=inet_addr(IP);
        //成功返回0,失败返回-1设置错误码,设置成功只能接收来自IP主机发送给PORT的信息
        int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
        if(result!=0){
            cout<<"绑定端口号失败"<<endl;
        }

3)发送与接收数据

发送数据,UDP协议使用的是sendto接口

socketfd就是套接字文件描述符,buf是发送的数据地址,len是发送数据的长度,flag是位图,使用|可以实现对发送的方法控制

  • 发送标志,可以是一个或多个标志的组合,用于修改 sendto 的行为。常见的标志包括:
    • MSG_CONFIRM:请求确认消息已被接收(某些实现可能不支持)。
    • MSG_DONTROUTE:避免路由,直接发送到本地接口。
    • MSG_DONTWAIT:非阻塞发送,如果操作会阻塞,则立即返回错误。
    • MSG_EOR:表示记录结束(对某些协议有意义)。
    • MSG_MORE:指示发送的数据是更大消息的一部分。

后面两个参数就不必多少,目标地址的信息和长度强转得来。最后成功返回发送数据的长度,失败返回-1,并设置错误码。

接收数据,UDP协议用的是recvfrom函数,

socketfd就是套接字文件描述符,buf是接收数据存放的地址,len是接收数据的最大长度,flag是位图,使用|可以实现对接收数据的方法控制

  • 接收标志,可以是一个或多个标志的组合,用于修改 recvfrom 的行为。常见的标志包括:
    • MSG_PEEK:查看数据而不从队列中删除它。
    • MSG_WAITALL:请求接收完整的消息(对于某些协议可能不适用)。
    • MSG_DONTWAIT:非阻塞接收,如果操作会阻塞,则立即返回错误。
    • MSG_TRUNC:即使数据被截断也继续接收(通常与 MSG_PEEK 一起使用)。
    • MSG_CTRUNC:如果控制消息被截断,则设置 msg_flags 的 MSG_CTRUNC 标志。

src_addr会返回发送数据的信息,如端口号,IP地址,addrlen是src_addr的长度。成功返回收到数据的长度,失败返回-1。

4)UDP简单的发送数据和接收数据服务器

中间可能有一个地方没讲清楚,bind函数不论接收,数据还是发送数据都最好设置,设置成功能接收你设置的主机发过来的特点端口号消息,sendto函数里面设置的是要发送给的人的IP和端口号。recvfrom函数里面的struct sockeaddr是接收消息的发送主机信息,方便你回信息和处理。

发送端

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       #include<unistd.h>
       #include <arpa/inet.h>
       #include <netinet/in.h>
       #include<iostream>
       using namespace std;

       #define PORT 8081
       //本地环回通信测试
       #define IP "127.0.0.1"
       int main(){
        //AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
        int fd=socket(AF_INET,SOCK_DGRAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
            return -1;
        }
        //不绑定端口号,操作系统随机分配
        char msg[13]="hello world!";
        struct sockaddr_in _send; 
        _send.sin_family=AF_INET;
        _send.sin_port=htons(8080);
        _send.sin_addr.s_addr=inet_addr(IP);
        //给地址为IP主机8080端口号发送消息
        int result=sendto(fd,(void*)msg,13,0,(struct sockaddr*)&_send,(socklen_t)sizeof(_send));
        if(result<0){
            cout<<"发送数据失败"<<endl;
            return -1;
        }
        close(fd);
        return 0;
       }

接收端

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       #include<unistd.h>
       #include <arpa/inet.h>
       #include <netinet/in.h>
       #include<iostream>
       using namespace std;
        #define PORT 8080
       int main(){
        //AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
        int fd=socket(AF_INET,SOCK_DGRAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
            return -1;
        }
        //IPV4结构体
        struct sockaddr_in _addr; 
        //设置为IPV4协议
        _addr.sin_family=AF_INET;
        //端口号网络字节序
        _addr.sin_port=htons(PORT);
        //接收所有主机的信息
        _addr.sin_addr.s_addr=INADDR_ANY;
        //成功返回0,失败返回-1设置错误码
        int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
        if(result!=0){
            cout<<"绑定端口号失败"<<endl;
            return -1;
        }
        char msg[20];
        struct sockaddr_in recv;
        //必须写,不能为空。
        socklen_t len=sizeof(recv);
        result=recvfrom(fd,(void*)msg,20,0,(struct sockaddr*)&recv,&len);
        if(result<0){
            cout<<"接收数据数据失败"<<endl;
            return -1;
        }
        for(int i=0;i<result;i++){
            cout<<msg[i];
        }
        close(fd);
        return 0;
       }

二,TCP编程

1)创建套接字

创建套接字,与UDP创建套接字相似,只要把SOCK_DGRAM改为SOCK_STREAM

        //SOCK_STREAM代表字节流,适用于TCP
        int fd=socket(AF_INET,SOCK_STREAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
            return -1;
        }

2)绑定端口号

绑定端口号与UDP没有差别,就是接收来自指定的主机的连接请求,UDP是没有连接,需要发送消息时指定目的地址的。暂时简单理解就行。

struct sockaddr_in _addr; 
        //设置为IPV4协议
        _addr.sin_family=AF_INET;
        //端口号网络字节序
        _addr.sin_port=htons(PORT);
        //IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
        _addr.sin_addr.s_addr=inet_addr(IP);
        //成功返回0,失败返回-1设置错误码
        int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
        if(result!=0){
            cout<<"绑定端口号失败"<<endl;
            return -1;
        }

3)使套接字进入监听状态

在TCP编程里面,创建套接字后并不能直接使用,TCP套接字只用来接收来自其他主机的连接请求,UDP发送完数据就不管了,是无连接的,TCP是面向连接的,双方会建立一个连接,也就是会为两台主机间创建单独的文件描述符,并且进行管理,这个文件描述符只能用来双方通信,而UDP可以实现一个文件描述符也就是socket就向所有主机发送消息。只有将套接字变成监听状态才会接收来自其他主机的连接。

第一个参数无需多言,就是我们使用socket函数创建的套接字,backlog是允许同时与多少台主机建立连接,也就是同时创建多少个通信的文件描述符,成功返回0,失败返回-1,并设置错误码。

        //允许同时最大与三个主机建立连接
        result=listen(fd,3);
        if(result!=0){
            cout<<"套接字启动监听失败"<<endl;
            return -1;
        }

4)获取成功建立连接的的文件描述符和主机信息

套接字进入监听状态后,我们需要获得建立连接的文件描述符,这样基于文件描述符才能和建立连接的主机通信,我们使用accept函数获取建立连接的消息,一般使用一个while循环来获取得到的多个连接信息。

第一个参数是套接字,第二个参数是连接主机的信息,第三个是第二个参数的长度,方便区分类型。成功返回建立连接的文件描述符,失败返回-1,并设置错误码。

        while(1){
            //这里不对对方主机信息进行处理,设置为空
            int fd_net=accept(fd,NULL,NULL);
            if(fd_net==-1){
            cout<<"TCP连接失败"<<endl;
            return -1;
            }
            //进行处理,发送或者接收数据
        }

5)发送与接收数据

TCP可以使用UDP的sendto和recvfrom函数发送与接收数据,但一般不这么做,因为TCP以及建立连接了,每个连接文件描述符都只和一台主机通信,被唯一的四元组来标识的,这个四元组包括源IP地址、源端口号、目的IP地址和目的端口号。没必要使用这两个函数,这两个函数里面还需要包括目的主机地。一般使用send和write,read与recv。

flag常用标志

  1. MSG_DONTWAIT(或MSG_NONBLOCK)
    • 作用:允许非阻塞操作。如果套接字被设置为非阻塞模式,并且发送缓冲区已满,则send函数会立即返回,而不是阻塞等待缓冲区空间可用。
    • 返回值:在非阻塞模式下,如果发送缓冲区已满,send函数可能返回-1,并设置errnoEAGAINEWOULDBLOCK,表示资源暂时不可用。
  2. MSG_OOB(Out-of-Band Data)
    • 作用:发送带外数据。带外数据通常用于发送紧急数据,这些数据会被接收方优先处理。然而,并非所有协议都支持带外数据,且其使用方式可能因协议而异。
    • 限制MSG_OOB标志通常仅适用于流式套接字(如SOCK_STREAM),而不适用于数据报套接字(如SOCK_DGRAM)。
  3. MSG_DONTROUTE
    • 作用:勿将数据路由出本地网络。这个标志告诉系统不要通过网关或路由器发送数据,而只在本地网络上发送。然而,并非所有系统都支持这个标志,且其效果可能因系统而异。

成功返回发送数据大小,失败返回-1,设置错误码。

fd是文件描述符,也就是accept函数的返回值,buf被发送的数据,count是发送的大小。

flag常用标志

  1. MSG_PEEK
    • 作用:查看接收队列中的数据,但不从队列中移除它们。这允许调用者在不实际消耗数据的情况下检查是否有数据可读。
    • 使用场景:在需要多次读取同一份数据或检查数据是否到达时非常有用。
  2. MSG_WAITALL
    • 作用:阻塞调用,直到接收到指定长度的数据或连接关闭。然而,需要注意的是,并非所有系统都支持这个标志,且其行为可能因系统而异。
    • 使用场景:在需要确保接收到完整消息时非常有用,但应谨慎使用,因为它可能导致程序在数据不足时长时间阻塞。
  3. MSG_DONTWAIT(或MSG_NONBLOCK
    • 作用:在非阻塞模式下接收数据。如果当前没有数据可读,则立即返回,而不是阻塞等待。
    • 使用场景:在需要避免阻塞等待数据到达时非常有用,例如在非阻塞I/O或事件驱动的编程模型中。
  4. MSG_OOB
    • 作用:接收带外数据(Out-of-Band Data)。带外数据通常用于发送紧急数据,这些数据会被接收方优先处理。然而,并非所有协议都支持带外数据。
    • 使用场景:在需要处理紧急数据或优先级较高的消息时非常有用,但应确保所使用的协议支持带外数据。
  5. MSG_TRUNC
    • 作用:如果接收到的数据长度超过了缓冲区长度,则只返回缓冲区长度的数据,并截断多余的数据。然而,需要注意的是,并非所有系统都支持这个标志。
    • 使用场景:在需要限制接收数据的大小或处理不完整数据时可能有用。
  6. MSG_CTRUNC
    • 作用:类似于MSG_TRUNC,但用于控制信息的截断。如果接收到的控制信息长度超过了缓冲区长度,则只返回缓冲区长度的控制信息。
    • 使用场景:在处理带有控制信息的套接字时可能有用。
  7. MSG_ERRQUEUE
    • 作用:接收错误信息。如果接收到的数据包出现错误,则会将错误信息放入错误队列中,可以通过此标志来接收这些错误信息。
    • 使用场景:在需要处理套接字错误或诊断网络问题时非常有用。

fd是文件描述符,也就是accept函数的返回值,buf存放数据,count是接收数据的最大大小,防止越界。

6)连接其他主机

上面我们只说了如何被动连接其他主机,但我们该如何主动连接其他主机呢?使用connect函数我们主动连接其他主机,是需要设置协议和IP,端口号信息的。注意connect连接成功之后这个scokfd就被占用了,用来后续的通信,需要继续使用socket函数创建与多台主机建立连接。这是与accept函数不同的地方,accept函数是创建了新的文件描述符,sockfd还可以继续监听。

成功返回0,失败返回-1,其他这些前面都讲过,老生常谈了,无需多言。

7)TCP简单的发送数据和接收数据服务器

服务端

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       #include<unistd.h>
       #include <arpa/inet.h>
       #include <netinet/in.h>
       #include<iostream>
       using namespace std;
       #define PORT 8080
       #define IP "127.0.0.1"
       int main(){
        //SOCK_STREAM代表字节流,适用于TCP
        int fd=socket(AF_INET,SOCK_STREAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
            return -1;
        }
        //IPV4结构体
        struct sockaddr_in _addr; 
        //设置为IPV4协议
        _addr.sin_family=AF_INET;
        //端口号网络字节序
        _addr.sin_port=htons(PORT);
        //IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
        _addr.sin_addr.s_addr=inet_addr(IP);
        //成功返回0,失败返回-1设置错误码
        int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
        if(result!=0){
            cout<<"绑定端口号失败"<<endl;
            return -1;
        }
        //允许同时最大与三个主机建立连接
        result=listen(fd,3);
        if(result!=0){
            cout<<"套接字启动监听失败"<<endl;
            return -1;
        }
        while(1){
            //这里不对对方主机信息进行处理,设置为空
            int fd_net=accept(fd,NULL,NULL);
            if(fd_net==-1){
            cout<<"TCP连接失败"<<endl;
            return -1;
            }
            char msg[13]="hello world!";
            //进行处理,发送或者接收数据
            result=send(fd_net,(void*)msg,13,0);
            close(fd);
        }
        return 0;
       }

客户端

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       #include<unistd.h>
       #include <arpa/inet.h>
       #include <netinet/in.h>
       #include<iostream>
       using namespace std;
       #define PORT 8080
       #define IP "127.0.0.1"
       int main(){
        //SOCK_STREAM代表字节流,适用于TCP
        int fd=socket(AF_INET,SOCK_STREAM,0);
        if(fd<0){
            cout<<"创建套接字失败"<<endl;
            return -1;
        }
        //IPV4结构体
        struct sockaddr_in _addr; 
        //设置为IPV4协议
        _addr.sin_family=AF_INET;
        //端口号网络字节序
        _addr.sin_port=htons(PORT);
        _addr.sin_addr.s_addr=inet_addr(IP);
        int result=connect(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
        if(result==-1){
            cout<<"连接主机失败"<<endl;
            return -1;
        }
        char msg[20];
        result=recv(fd,msg,20,0);
        for(int i=0;i<result;i++){
            cout<<msg[i];
        }
        close(fd);
        return 0;
       }

创造不易,我为人人,人人为我,如果大家有所收获的话可以点赞加关注,下一篇文章将会着重讲TCP与UDP的特性。


http://www.kler.cn/a/417047.html

相关文章:

  • 群控系统服务端开发模式-应用开发-前端短信配置开发
  • 前端学习week8——vue.js
  • llamaindex实战-ChatEngine-ReAct Agent模式
  • 搭建环境-网络基本概念详解
  • 【数据结构】ArrayList与顺序表
  • SpringBoot连接测试InfluxDB时序数据库
  • Windows 平台使用 podofo.dll 异常,需要安装一下库:Win64OpenSSL_Light-3_3_2.msi
  • 配置泛微e9后端开发环境
  • 学习C#中的反射
  • 【Yarn Bug】 yarn 安装依赖出现的网络连接问题
  • Java抛出自定义运行运行
  • 后端-mybatis的一对一查询
  • 准确--在 AlmaLinux 9.2 上快速搭建 FTP 服务器
  • AI潮汐日报1128期:马斯克计划推出Grok挑战GPT宝座、实时数字孪生心脏模拟、大模型竟也会产生焦虑和偏见
  • SpringBoot 架构的新冠密接者跟踪系统:安全防护体系深度解读
  • 学习ASP.NET Core的身份认证(基于Session的身份认证3)
  • Next.js 中 API 路由与 Actions 的使用选择与比较
  • linux centos nginx编译安装
  • 【人工智能-科普】深度森林:传统机器学习与深度学习的创新结合
  • 云原生自动化测试之命令行工具自动化测试设计思路分享
  • 【开源免费】基于Vue和SpringBoot的新闻推荐系统(附论文)
  • 深度学习:GPT-2的MindSpore实践
  • 【干货分享】Boosting算法简单案例
  • 中国如何应对非传统网络安全风险?
  • html无序列表怎么去掉小黑点
  • Ps:存储 Adobe PDF - 压缩