热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

socket网络编程之TCP、UDP

之前说的用于进程间通信的几种方式:消息signal、管道pipe、消息队列msg、共享内存shm、信号量sem。都只适用于一台主机上的进程间通信,那么如何

之前说的用于进程间通信的几种方式:消息signal、管道pipe、消息队列msg、共享内存shm、信号量sem。都只适用于一台主机上的进程间通信,那么如何实现两台计算机之间的进程通信呢?所以,来了解一下异地进程通信

1 异地进程通信

  • 协议层为双方的主机通信进程分配“端口”和缓冲区,以便异地进程间的通信。

    1.1TCP/IP协议

    以下是OSI参考模型与TCP/IP参考模型的对应关系:
    socket网络编程之TCP、UDP
    socket网络编程之TCP、UDP

    1.1.1 TCP/IP协议族

    TCP/IP 协议组大体上分为三部分:
    1.Internet 协议(IP)
    2.传输控制协议(TCP)和用户数据报文协议(UDP)
    3.处于TCP 和UDP 之上的一组协议专门开发的应用程序。它们包括:TELNET,文件传送协议(FTP),域名服务(DNS)和简单的邮件传送程序(SMTP)等许多协议。
    应用层协议

  • Telnet
  • 文件传送协议(FTP和TFTP)
  • 简单的文件传送协议(SMTP)
  • 域名服务(DNS)等协议

    2 网络编程基础

  • socket标准被扩展成window socket和unix socket
  • linux中的网络编程通过socket接口实现。
  • Socket既是一种特殊的IO,它也是一种文件描述符
  • 一个完整的Socket 都有一个相关描述{协议,本地地址,本地端口,远程地址,远程端口};每一个Socket 有一个本地的唯一Socket 号,由操作系统分配。

    2.1 SOCKET分类

    流式套接字(SOCK_STREAM)
    流式的套接字可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP 保证了数据传输的正确性和顺序性。
    数据报套接字(SOCK_DGRAM)
    数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。使用数据报协议UDP协议。
    原始套接字。
    原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。

    2.2 编程流程

    TCP
    socket网络编程之TCP、UDP
    UDP
    socket网络编程之TCP、UDP
    具体函数的用法,就自己man了。

    2.2.1 套接字地址结构

    重点讲一下套接字地址结构:

    #include 
    struct sockaddr
    {
    unsigned short sa_family; /* address族, AF_xxx */
    char sa_data[14];     /* 14 bytes的协议地址 */
    };
  • sa_family的取值,一般来说,IPV4使用“AF_INET”
  • sa_data包含了一些远程电脑的地址、端口和套接字的数目,里面的数据是杂溶在一起的。一般我们不用这个结构体,因为我们一般使用的地址都是IP+端口号。比如:IP192.168.159.2 port3306 。这样来记录地址。所以一般使用下面这个地址结构,而知数据类型是等效的,可以互相转换。
    #include 
    struct sockaddr_in {
    short int sin_family; /* Internet地址族 */
    unsigned short int sin_port; /* 端口号 */
    struct in_addr sin_addr; /* Internet地址 */
    unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
    };

    2.2.2 字节序列转换

  • 因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后 ),而网络传输的数据大家是一定要统一顺序的。所以对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换。
  • htons()——“Host to Network Short”
    主机字节顺序转换为网络字节顺序(对无符号短型进行操作2bytes)
  • htonl()——“Host to Network Long” 
    主机字节顺序转换为网络字节顺序(对无符号长型进行操作4bytes)
  • ntohs()——“Network to Host Short”
    网络字节顺序转换为主机字节顺序(对无符号短型进行操作2bytes)
  • ntohl()——“Network to Host Long ”
    网络字节顺序转换为主机字节顺序(对无符号长型进行操作4bytes)

    2.2.3地址格式转换

    -linux提供将点分格式的地址转于长整型数之间的转换函数。

  • inet_addr()能够把一个用数字和点表示IP 地址的字符串转换成一个无符号长整型。
  • inet_ntoa()能够把网络字节顺序转换为地址结构的数据。

    2.2.4基本套接字调用

    socket() bind() connect()
    listen() accept() send()
    recv() sendto() shutdown()
    recvfrom() close() getsockopt()
    setsockopt() getpeername()
    getsockname() gethostbyname()
    gethostbyaddr() getprotobyname()
    fcntl()

    练习1-TCP

    TCP连接,等待客户端输入,将内容发送给服务器,并获取客户端地址。
    这里,getsocketname()表示获得本地(自己)的地址;
    getpeername()表示获得连接上的客户端的地址(源IP地址)。


    server.c

    #include      
    #include 
    #include     //sockaddr_in
    #include 
    #include 
    
    int main()
    {
        int fd;
        int clientfd;
        int ret;
        pid_t pid;
        int addrLen = 0;
        char acbuf[20] = "";
        struct sockaddr_in addr = {0};  //自己的地址
        struct sockaddr_in clientAddr = {0};    //连上的客户端的地址
    
        //1.socket()
        fd = socket(PF_INET,SOCK_STREAM,0);
        if(fd == -1)
        {
            perror("socket");
            return -1;
        }
    
        //2.bind()
        addr.sin_family = AF_INET;
        addr.sin_port = htons(1234);
        addr.sin_addr.s_addr = inet_addr("192.168.159.6");
        ret = bind(fd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in));
        if(ret == -1)
        {
            perror("bind");
            return -1;
        }
    
        //3.listen()
        ret = listen(fd,10);
        if(ret == -1)
        {
            perror("listen");
            return -1;
        }
    
        //4.阻塞 等待 accept()
        clientfd = accept(fd,NULL,NULL);
        if(clientfd == -1)
        {
            perror("accept");
            return -1;
        }
    
    //获取客户端地址
    addrLen = sizeof(struct sockaddr_in);
    ret = getpeername(clientfd, (struct sockaddr *)&clientAddr, &addrLen);
    if(ret == -1)
    {
        perror("getpeername");
        return -1;
    }
    printf("client login.\nip: %s , port: %d\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
    
    //5.通信
    while(1)
    {
        memset(acbuf,0,20);
        if (read(clientfd,acbuf,20) > 0)
        {
            printf("receive: %s\n",acbuf);
        }
    
    }
    
    //6.close()
    close(fd);
    return 0;
    }

client.c

#include      
#include 
#include  //sockaddr_in
#include 
#include 

int main()
{
    int fd;
    int ret;
    char acbuf[20] = "";
    struct sockaddr_in serAddr = {0};

    //1.socket();
    fd = socket(PF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    //2.连接connect() 服务器的地址
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(1234);
    serAddr.sin_addr.s_addr = inet_addr("192.168.159.6");
    ret = connect(fd,(struct sockaddr *)&serAddr,sizeof(struct sockaddr_in));
    if(ret == -1)
    {
        perror("connect");
        return -1;
    }

    //3.通信
    while(1)
    {
        printf("send: ");
        fflush(stdout);
        scanf("%s",acbuf);
        if(strcmp(acbuf,"exit") == 0)
        {
            break;
        }
        write(fd,acbuf,strlen(acbuf));
    }

    //4.close()
    close(fd);
    return 0;
}

运行结果:
socket网络编程之TCP、UDP
做个改进,以上代码,只能一个客户端连接上。因为TCP是基于点对点的,一个accept()对应一个connnect()。要想连接多个客户端,就得使用fork(),一个进程用来专门阻塞等待客户端的连接,一个用来处理与已连接上客户端的通信。
代码如下:

server.c

int main()
    {
        int fd;
        int clientfd;
        int ret;
        pid_t pid;
        int addrLen = 0;
        char acbuf[20] = "";
        char client_addr[100] = "";
        struct sockaddr_in addr = {0};  //自己的地址
        struct sockaddr_in clientAddr = {0};    //连上的客户端的地址

        signal(SIGCHILD,SIG_IGN);

        //1.socket()
        fd = socket(PF_INET,SOCK_STREAM,0);
        if(fd == -1)
        {
            perror("socket");
            return -1;
        }

        //会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
        //由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
        //if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) <0)
            //{
        //      perror("setsockopet error\n");
        //      return -1;
            //}

        //2.bind()
        addr.sin_family = AF_INET;
        addr.sin_port = htons(1234);
        addr.sin_addr.s_addr = inet_addr("192.168.159.6");
        ret = bind(fd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in));
        if(ret == -1)
        {
            perror("bind");
            return -1;
        }

        //3.listen()
        ret = listen(fd,10);
        if(ret == -1)
        {
            perror("listen");
            return -1;
        }

        while(1)        
        {
            //4.阻塞等待 accept()
            clientfd = accept(fd,NULL,NULL);
            if(clientfd == -1)
            {
                perror("accept");
                return -1;
            }

            pid = fork();   //父进程负责继续监听等待,子进程父子与已连接客户端通信

            if(pid == -1)
            {
                perror("fork");
                return -1;
            }
            if(pid == 0)    //子进程
            {
                //获取客户端地址
                addrLen = sizeof(struct sockaddr_in);
                ret = getpeername(clientfd, (struct sockaddr *)&clientAddr, &addrLen);
                if(ret == -1)
                {
                    perror("getpeername");
                    return -1;
                }
                sprintf(client_addr,"ip: %s , port: %d\n",\
                    inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
                printf("client longin.\n%s\n",client_addr);

                //5.通信
                while(1)
                {
                    memset(acbuf,0,20);
                    if (read(clientfd,acbuf,20) == 0)   //客户端退出
                    {
                        //结束相应的server进程
                        close(clientfd);
                        exit(0);    //僵尸进程
                    }
                    printf("from %sreceive : %s\n\n",client_addr,acbuf);
                }
            }
            else    //父进程
            {
                //返回while,继续等待
            }

        }

        //6.close()
        close(fd);
        return 0;
    }

这里一定要注意,每结束一个客户端,一定要关掉相应的文件描述符,并且结束掉子进程(僵尸进程),不然,随着客户端的增加,进程数会越来越大
client.c

int main()
{
    int fd;
    int ret;
    int addrLen;
    char acbuf[20] = "";
    struct sockaddr_in serAddr = {0};
    struct sockaddr_in myAddr = {0};

    //1.socket();
    fd = socket(PF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    //2.连接connect() 服务器的地址
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(1234);
    serAddr.sin_addr.s_addr = inet_addr("192.168.159.6");
    ret = connect(fd,(struct sockaddr *)&serAddr,sizeof(struct sockaddr_in));
    if(ret == -1)
    {
        perror("connect");
        return -1;
    }

    //获取自己的地址
    addrLen = sizeof(struct sockaddr_in);
    ret = getsockname(fd,(struct sockaddr *)&myAddr,&addrLen);
    if(ret == -1)
    {
        perror("getsockname");
        return -1;
    }
    printf("client---ip: %s , port: %d\n",\
                inet_ntoa(myAddr.sin_addr),ntohs(myAddr.sin_port));
    //3.通信
    while(1)
    {
        printf("send: ");
        fflush(stdout);
        scanf("%s",acbuf);
        if(strcmp(acbuf,"exit") == 0)
        {
            break;
        }
        write(fd,acbuf,strlen(acbuf));
    }

    //4.close()
    close(fd);
    return 0;
}

运行结果:
socket网络编程之TCP、UDP

练习2-UDP

使用UDP连接,完成上述内容。但是发现,使用UDP,因为是面向无连接的,所以在没有收到或者发送包之前,是无法得知源IP地址的。
那UDP如何知道客户端的IP地址和端口呢?
1、由客户端显示地高速服务器IP地址和端口,发消息。
2、隐式的。服务器从收到的包头中得到源IP和端口号。
server.c

int main()
{
    int sockfd;
    int ret;
    char acbuf[20] = "";
    char client_addr[100] = "";
    struct sockaddr_in addr = {0};
    struct sockaddr_in clientAddr = {0};
    int addrLen = sizeof(struct sockaddr_in);
    int reuse = 0;

    //1.socket()
    sockfd = socket(PF_INET,SOCK_DGRAM,0);
    if(sockfd == -1)
    {
        perror("socket");
        return -1;
    }

    //2.bind()
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1235);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    ret = bind(sockfd,(struct sockaddr *)&addr,addrLen);
    if(ret == -1)
    {
        perror("bind");
        return -1;
    }

    //3.通信
    while(1)
    {
        memset(acbuf,0,20);
        if(recvfrom(sockfd, acbuf, 100,0,(struct sockaddr *)&clientAddr,&addrLen) == -1)
        { 
            perror("recvfrom"); 
            return -1;
         } 
         //收到客户端的数据包之后,就可以知道客户端地址
        sprintf(client_addr," ip: %s , port: %d\n",\
                inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
         printf("receive from %s: %s\n",client_addr,acbuf);

    }

    //4.close
    close(sockfd);

    return 0;
}

client.c

int main()
{
    int sockfd;
    int ret;
    int addrLen = sizeof(struct sockaddr_in);
    char acbuf[20] = "";
    struct sockaddr_in serAddr = {0};
    struct sockaddr_in myAddr = {0};

    //1.socket()
    sockfd = socket(PF_INET,SOCK_DGRAM,0);
    if(sockfd == -1)
    {
        perror("socket");
        return -1;
    }
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(1235);
    serAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    //2.通信
    while(1)
    {
        printf("send: ");
        fflush(stdout);
        scanf("%s",acbuf);
        if(strcmp(acbuf,"exit") == 0)
        {
            break;
        }
        sendto(sockfd, acbuf, 20,0,(struct sockaddr *)&serAddr,addrLen);

        //获取自己的地址
        ret = getsockname(sockfd,(struct sockaddr *)&myAddr,&addrLen);
        if(ret == -1)
        {
            perror("getsockname");
            return -1;
        }
        printf("client---ip: %s , port: %d\n\n",\
                    inet_ntoa(myAddr.sin_addr),ntohs(myAddr.sin_port));

    }

    //3.close
    close(sockfd);

    return 0;
}

运行结果:
socket网络编程之TCP、UDP
会发现,此时是可以直接运行多个客户端的,因为,UDP是面向无连接的,可以是一对多,多对一,多对多的,只要客户端知道服务器地址,就可以连上。


Ps:本人理解有限,还未学习完,有错请指出。


推荐阅读
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 通过Anaconda安装tensorflow,并安装运行spyder编译器的完整教程
    本文提供了一个完整的教程,介绍了如何通过Anaconda安装tensorflow,并安装运行spyder编译器。文章详细介绍了安装Anaconda、创建tensorflow环境、安装GPU版本tensorflow、安装和运行Spyder编译器以及安装OpenCV等步骤。该教程适用于Windows 8操作系统,并提供了相关的网址供参考。通过本教程,读者可以轻松地安装和配置tensorflow环境,以及运行spyder编译器进行开发。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • {moduleinfo:{card_count:[{count_phone:1,count:1}],search_count:[{count_phone:4 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 解决github访问慢的问题的方法集锦
    本文总结了国内用户在访问github网站时可能遇到的加载慢的问题,并提供了解决方法,其中包括修改hosts文件来加速访问。 ... [详细]
  • 在2022年,随着信息化时代的发展,手机市场上出现了越来越多的机型选择。如何挑选一部适合自己的手机成为了许多人的困扰。本文提供了一些配置及性价比较高的手机推荐,并总结了选择手机时需要考虑的因素,如性能、屏幕素质、拍照水平、充电续航、颜值质感等。不同人的需求不同,因此在预算范围内找到适合自己的手机才是最重要的。通过本文的指南和技巧,希望能够帮助读者节省选购手机的时间。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
author-avatar
好人cuiyin_550
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有