zoukankan      html  css  js  c++  java
  • socket通信基础

    各位兄弟,在学习Linux编程基础之前,一定要先学习Linux基础知识和计算机网络基础知识,如果对这两方面的基础知识和基本概念不熟,谈不上Linux编程和网络通信编程。

    一、socket通信的概念

    socket也称作“套接字”,描述了计算机的IP地址和端口,运行在计算机中的程序之间采用socket进行数据通信。通信的两端都有socket,它是一个通道,数据在两个socket之间进行传输。

    socket把复杂的TCP/IP协议族隐藏在socket接口后面,对程序员来说,只要用好socket相关的函数,就可以完成数据通信。

    二、套接字(socket)

    TCP提供了流(stream)和数据报(datagram)两种通信机制,所以套接字也分为流套接字和数据报套接字。

    流套接字的类型是SOCK_STREAM,它提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有出错后重新发送的机制(就像两个人在打电话,聊天您一句我一句,有来有往,没听清楚就再说一次)。

    数据报套接的类型是SOCK_DGRAM,它不需要建立和维持一个连接,采用UDP/IP协议实现。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它不需要建立和维持连接(就像一个人向另一个人发短信,一条短信发出去,对方不一定能收到)。

    在实际开发中,数据报套接字(即UDP)的应用场景极少,本章节只介绍流套接字。

    三、socket通信的过程

    1)服务端程序将一个套接字绑定到指定的ip地址和端口,并通过此套接字等待和监听客户的连接请求。

    2)客户程序向服务端程序绑定的地址和端口发出连接请求。

    3)服务端接受连接请求。

    4)客户端和服务端通过读写套接字进行通信。

    在这里插入图片描述

    四、客户/服务端模式

    在TCP/IP网络应用中,两个程序之间通信模式是客户/服务端模式(client/server),客户/服务端也叫作客户/服务器,各人习惯。

    服务端的工作流程

    1)创建服务端的socket。

    2)把服务端用于通信的地址和端口绑定到socket上。

    3)把socket设置为监听模式。

    4)接受客户端的连接。

    5)与客户端通信,接收客户端发过来的报文后,回复处理结果。

    6)不断的重复第5)步,直到客户端断开连接。

    7)关闭socket,释放资源。

    服务端示例(book242.cpp)

    /*
     * 程序名:book242.cpp,此程序用于演示socket通信的服务端
     * 作者:C语言技术网(www.freecplus.net) 日期:20190525
    */
    #include "_public.h"
    
    int main()
    {
      // 第1步:创建服务端的socket。
      int listenfd = socket(AF_INET,SOCK_STREAM,0);  
    
      // 第2步:把服务端用于通信的地址和端口绑定到socket上。
      struct sockaddr_in servaddr;    // 服务端地址信息的数据结构。
      memset(&servaddr,0,sizeof(servaddr));
      servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET。
      servaddr.sin_addr.s_addr = htonl(INADDR_ANY);          // 任意ip地址。
      //servaddr.sin_addr.s_addr = inet_addr("118.89.50.198"); // 指定ip地址。
      servaddr.sin_port = htons(5051);  // 指定通信端口。
      if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
      { perror("bind"); close(listenfd); return -1; }
    
      // 第3步:把socket设置为监听模式。
      if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
    
      // 第4步:接受客户端的连接。
      int  clientfd;                  // 客户端的socket。
      int  socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
      struct sockaddr_in clientaddr;  // 客户端的地址信息。
      clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
      printf("客户端(%s)已连接。
    ",inet_ntoa(clientaddr.sin_addr));
    
      // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
      char buffer[1024];
      while (1)
      {
        memset(buffer,0,sizeof(buffer));
        if (recv(clientfd,buffer,sizeof(buffer),0)<=0) break;   // 接收客户端的请求报文。
        printf("接收:%s
    ",buffer);
    
        strcpy(buffer,"ok");
        if (send(clientfd,buffer,strlen(buffer),0)<=0) break;   // 向客户端发送响应结果。
        printf("发送:%s
    ",buffer);
      }
    
      // 第6步:关闭socket,释放资源。
      close(listenfd); close(clientfd);  
    }
    

    2、客户端的工作流程

    1)创建客户端的socket。

    2)向服务器发起连接请求。

    3)与服务端通信,发送一个报文后等待回复,然后再发下一个报文。

    4)不断的重复第3)步,直到全部的数据被发送完。

    5)第4步:关闭socket,释放资源。

    客户端示例(book241.cpp)

    /*
     * 程序名:book241.cpp,此程序用于演示socket的客户端
     * 作者:C语言技术网(www.freecplus.net) 日期:20190525
    */
    #include "_public.h"
    
    int main()
    {
      // 第1步:创建客户端的socket。
      int sockfd = socket(AF_INET,SOCK_STREAM,0); 
    
      // 第2步:向服务器发起连接请求。
      struct hostent* h; 
      if ( (h = gethostbyname("118.89.50.198")) == 0 )   // 指定服务端的ip地址。
      { perror("gethostbyname"); close(sockfd); return -1; }
      struct sockaddr_in servaddr;
      memset(&servaddr,0,sizeof(servaddr));
      servaddr.sin_family = AF_INET;
      servaddr.sin_port = htons(5051); // 指定服务端的通信端口。
      memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
      if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)  // 向服务端发起连接清求。
      { perror("connect"); close(sockfd); return -1; }
    
      char buffer[1024];
      
      // 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
      for (int ii=0;ii<3;ii++)
      {
        memset(buffer,0,sizeof(buffer));
        sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
        if (send(sockfd,buffer,strlen(buffer),0)<=0) break;    // 向服务端发送请求报文。
        printf("发送:%s
    ",buffer);
        
        memset(buffer,0,sizeof(buffer));
        if (recv(sockfd,buffer,sizeof(buffer),0)<=0) break;    // 接收服务端返回的结果。
        printf("接收:%s
    ",buffer);
      }
    
      // 第4步:关闭socket,释放资源。
      close(sockfd);
    }
    

    在运行程序之前,必须保证服务器的防火墙已经开通了网络访问策略,如果您不明这句话的意思,说明您的Linux基础知识不够,请先学习Linux基础知识之后再来学习socket通信。

    先启动服务端程序book242,服务端启动后,进入等待客户端连接状态,然后启动客户端。

    客户端的输出如下:

    在这里插入图片描述
    服务端的输出如下:

    在这里插入图片描述

    五、注意事项

    1、别去纠缠细节

    在socket通信的客户端和服务器的程序里,出现了多种数据结构,调用了多个函数,涉及到很多方面的知识,对初学者来说,更重要的是了解socket通信的过程、每段代码的用途和函数调用的功能,不要去纠缠这些结构体和函数的参数,这些函数和参数虽然比较多,但可以修改的非常少,别抄错就可以了,需要注意的地方我会提出。

    2、服务端程序绑定地址

    如果服务器有多个网卡,多个IP地址,socket通信可以指定用其中一个地址来进行通信,也可以任意ip地址。

    1)指定ip地址的代码

    m_servaddr.sin_addr.s_addr = inet_addr("192.168.149.129"); // 指定ip地址
    

    2)任意ip地址的代码

    m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本主机的任意ip地址
    

    在实际开发中,采用任意ip地址的方式比较多。

    3、服务端程序绑定的通信端口

    m_servaddr.sin_port = htons(5000);  // 通信端口
    

    4、客户端程序指定服务端的ip地址

    struct hostent* h; // ip地址信息的数据结构
    if ( (h = gethostbyname("192.168.149.129")) == 0 )
    { perror("gethostbyname"); close(sockfd); return -1; }
    

    5、客户端程序指定服务端的通信端口

    servaddr.sin_port = htons(5000);
    

    6、send函数

    send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。

    函数声明:

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    

    sockfd为已建立好连接的socket。

    buf为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。

    len需要发送的数据的长度,为buf中有效数据的长度。

    flags填0, 其他数值意义不大。

    函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。

    注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。

    如果send函数返回的错误(<=0),表示通信链路已不可用。

    7、recv函数

    recv函数用于接收对端socket发送过来的数据。

    recv函数用于接收对端通过socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接收来自TCP连接的另一端发送过来数据。

    函数声明:

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    

    sockfd为已建立好连接的socket。

    buf为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。

    len需要接收数据的长度,不能超过buf的大小,否则内存溢出。

    flags填0, 其他数值意义不大。

    如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1,错误信息errno被标记。如果socket被对端关闭,返回值为0。

    如果recv函数返回的错误(<=0),表示通信通道已不可用。

    8、服务端有两个socket

    对服务端来说,有两个socket,一个是用于监听的socket,还有一个就是客户端连接成功后,由accept函数创建的用于与客户端收发报文的socket。

    9、程序退出时先关闭socket

    socket是系统资源,操作系统打开的socket数量是有限的,在程序退出之前必须关闭已打开的socket,就像关闭文件指针一样,就像delete已分配的内存一样,极其重要。

    值得注意的是,关闭socket的代码不能只在main函数的最后,那是程序运行的理想状态,还应该在main函数的每个return之前关闭。

    六、相关的库函数

    1、socket函数

    socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。socket函数用户客户端和服务端。

    函数声明:

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

    参数说明:

    domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

    type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

    protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

    说了一大堆废话,第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填0。

    除非系统资料耗尽,socket函数一般不会返回失败。

    2、gethostbyname函数

    把ip地址或域名转换为hostent 结构体表达的地址。

    函数声明:

    struct hostent *gethostbyname(const char *name);
    

    参数name,域名或者主机名,例如"192.168.1.3"、"www.freecplus.net"等。

    返回值:如果成功,返回一个hostent结构指针,失败返回NULL。

    gethostbyname只用于客户端。

    gethostbyname只是把字符串的ip地址转换为结构体的ip地址,只要地址格式没错,一般不会返回错误。函数失败不会设置errno的值。

    3、connect函数

    向服务器发起连接请求。

    函数声明:

    int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
    

    函数说明:connect函数用于将参数sockfd 的socket 连至参数serv_addr
    指定的服务端,参数addrlen为sockaddr的结构长度。

    返回值:成功则返回0, 失败返回-1, 错误原因存于errno 中。

    connect函数只用于客户端。

    如果服务端的地址错了,或端口错了,或服务端没有启动,connect一定会失败。

    4、bind函数

    服务端把用于通信的地址和端口绑定到socket上。

    函数声明:

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

    参数sockfd,需要绑定的socket。

    参数addr,存放了服务端用于通信的地址和端口。

    参数addrlen表示addr结构体的大小。

    如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。

    5、listen函数

    listen函数把主动连接套接字变为被动连接的套接字,使得这个socket可以接受其它socket的连接请求,从而成为一个服务端的socket。

    函数声明:

    int listen(int sockfd, int backlog);
    

    返回:0-成功, -1-失败

    参数sockfd是已经被bind过的套接字。socket函数返回的套接字是一个主动连接的套接字,在服务端的编程中,程序员希望这个套接字可以接受外来的连接请求,也就是被动等待客户端来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用listen函数来完成这件事。

    参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。

    当调用listen之后,服务端的套接字就可以调用accept来接受客户端的连接请求。

    listen函数一般不会返回错误。

    6、accept函数

    服务端接受客户端的连接。

    函数声明:

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

    参数sockfd是已经被listen过的套接字。

    参数addr用于存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可以填0。

    参数addrlen用于存放addr参数的长度,如果addr为0,addrlen也填0。

    accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称之为阻塞。

    accept等待到客户端的连接后,创建一个新的套接字,函数返回值就是这个新的套接字,服务端使用这个新的套接字和客户端进行报文的收发。

    accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。

    7、函数小结

    服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close

    客户端函数调用的流程是:socket->connect->send/recv->close

    其中send/recv可以进行多次交互。

    七、课后作业

    1)把book241.cpp和book242.cpp抄下来,编译运行,试试修改参数再运行。

    2)book241.cpp和book242.cpp程序中,有些代码不能动,有些代码可以动,把能动的都动一下,就算是抄代码,也要抄个明白。

    3)服务端的accept函数会阻塞,阻塞是专业名词,即等待,可以用代码测试一下。

    4)不管是服务端还是客户端recv函数也会阻塞,可以用代码测试一下。

    5)修改book241.cpp和book242.cpp,实现点对点的聊天功能,用户在客户端输入一个字符串,然后发送给服务端,服务端收到客户端的报文后,也提示用户输入一个字符串,返回给客户端,如果服务端收到客户端的报文是“bye”通信结束。

    6)如果以上作业都能完成,建议再把本文章的内容再看一次,对文章开始部分的理论知识将有新的理解。

    八、版权声明

    C语言技术网原创文章,转载请说明文章的来源、作者和原文的链接。
    来源:C语言技术网(www.freecplus.net)
    作者:码农有道

    如果文章有错别字,或者内容有错误,或其他的建议和意见,请您留言指正,非常感谢!!!

  • 相关阅读:
    SharePoint 2010 User Profile Sync Service自动停止
    如何区别多个svchost.exe?
    Log Parser分析IIS log的一个简单例子
    Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
    Windows中右键点击文件夹, 结果找不到共享选项卡, 怎么办?
    介绍SOS中的SaveModule命令
    SharePoint中Draft版本的文档不会收到document added的Alert Email
    和我一起学Windows Workflow Foundation(1)创建和调试一个WF实例
    门户网站
    C#基础—— check、lock、using语句归纳
  • 原文地址:https://www.cnblogs.com/wucongzhou/p/12498107.html
Copyright © 2011-2022 走看看