乐趣区

Linux-网络编程-一

一、网络编程基础

网络编程本身是一门很大的学问,涉及到的东西也很多,尤其是各种协议。先看图:

正如上图所示,网络编程中包含五大层面 (也有区分六个层面),从应用层到物理层可以明显看出 越往下越接近计算机硬件。自己并不是专业网络编程的工程师,所以仅对这五大层面有一点点粗浅的了解,这篇文章网络编程技巧博主写的比较详细. 平时大多数所谓网络编程,其实是在 传输层、网络层 方面.

二、socket 编程

首先,socket(套接字)编程应该属于 传输层,主要实现的是端到端的通信,非常类似于很久很久以前的固话通信,应用程序可以通过它发送或者接受数据,可以对它进行像文件似得读写、关闭等操作。套接字允许应用程序将 I / O 插入网络中,并与网络中的其他应用程序进行通信。

其次,网络中两台或多台主机之间进行通信,必须知道对应主机的地址,也就是其 IP 地址,但是只知道 IP 地址是远远不够的,试想如你在本机 A 发送了一个消息,另一个台主机 B 也接收到到了该消息,但是到底是 B 主机的哪一个进程接受并处理该消息?就像你用 QQ 给 B 发送消息,但是 B 不可能通过陌陌收到该消息。因此,相互通信的主机之间还必须确定一一对应的消息处理接口 – 端口。端口的存在,主要是为了确认消息一一对应性。另外,端口号其实就是一个从 0 开始的到 65535 之间的一个整型数字,0~1023 端口,也就是常说的静态端口,已被操作系统另做它用(http,https,ftp 等各种协议占用),我们自己所能使用的端口范围只能从 1024 开始,即动态端口取值[1024,65535].

可是看出,若要进行网络间通信,socket 至少要包含 IP+port 两个方面,其实事实也是如此. 还是以有线电话做为类比,socket 其实就是自己家中的一部电话,其中 IP 就是家庭地址,port 就是自己家的电话号码,当要给别人打电话时,别人家当然也必须有自己的座机和专属于该座机的号码.
或许我们也能猜出,socket 编程是网络编程里边必不可少且及其重要的一个环节.

三、Linux+socket 实践

1、目的

熟悉 Linux(这里用 Ubuntu16.04 版本,其他版本类似)下 socket 编程基本流程,掌握 socket 编程基本原理,搞懂 Linux 下 socket 编程所必须的函数及其用法.

实验:在本地模拟两台机器,服务器和客户端,服务器监听客户端信息并能发送广播,客户端可以主动给服务器发送消息, 其中消息的输入是从标准输入设备输入,并输出到标准输出 –Linux 终端.

开始之前必须了解一点 什么是 文件描述符 , 在 Unix Linux 系统中,文件描述符是一个非负整数,其存在作用更像一个索引,系统内核通过该 ” 索引 ” 找到对应的文件、设备、外设、安装的软件等等, 并通过 描述符 对它们进行操作。总而言之,文件描述符对应了系统上的所有文件,这里的 文件 并非 ” 传统意义上的普通文件 ”, 而是指 Linux 系统内核所能管理 1 的一切,包含文档、文件、硬件设备、系统软件等等。这也体现了 Linux 系统的设计思想 —- 把一切视作文件.

2、必要接口

1)、socket 函数

既然 socket 这么重要,来看它到底是个什么东西. 在 Linux 终端执行:man socket, 出现:

通过 Linux 手册查询可以知道该函数所必须的头文件,函数声明和函数描述等信息. 从 [DESCRIPTION] 字段可知,函数创建了一个用于通信的端点并返回该端点的 描述符 ,若创建成功,返回创建套接字的文件描述符,否则返回一负数.
函数声明 int socket(int domain,int type,int protocol);

参数 domain: 表示创建该 socket 所使用的通讯协议家族 – 地址族,现在一般用 IPv4 协议,所以通常会选择 AF_INET;
参数 type:指定所需的通信类型。包括数据流(SOCK_STREAM)<–>TCP 协议、数据报(SOCK-DGRAM)<–>UDP 协议和原始类型(S0CK_RAW)<–> 新网格协议的开发测试.
参数 protocol:说明该套接字使用的协议族中的特定协议。如果不希望特别指定使用的协议,则置为 0,使用默认的连接模式.
若要进行 基于 TCP IP 的网络开发测试,则函数创建方式一般为:

int listenfd = socket(AF_INET,SOCK_STREAM,0);

2)、bind 函数

既然有了一部“电话”,那么就需要为该电话绑定唯一的“所属地址”,同样 Linux 命令行执行:man bind, 同样函数声明为:

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

从手册的描述中可以看出,当成功创建 socket 套接字后,调用该函数可以将所创建的套接字 (sockfd) 和指定的地址 (addr) 绑定.
地址是由这样一个结构体指定:

struct sockaddr {
    sa_family_t sa_family;  // 地址族
    char sa_data[14];       //14 字节的协议地址
}

上面 struct sockaddr 是通用地址,在网络编程中 internet sockaddr 使用下面地址,两种地址可以互换:

struct sockaddr_in { 
    short int sin_family; /* 地址族,AF_xxx 在 socket 编程中只能是 AF_INET */ 
    unsigned short int sin_port; /* 端口号(使用网络字节顺序)*/ 
    struct in_addr sin_addr; /* 存储 IP 地址 4 字节 */ 
    unsigned char sin_zero[8]; /* 总共 8 个字节,实际上没有什么用,只是为了和 struct sockaddr 保持一样的长度 */ 
}; 

bind()函数的第三个参数表示地址所占字节长度,socklen_t本质上是一个 unsigned int宏定义.
可以通过这样方式指定地址:

    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

首先声明网络接口地址结构,在给该地址赋值前必须将其清空. 依次设置该地址的地址族、IP 和端口 (这里随便设置了一个),上边出现另一个新函数htons, 同样终端下man htons, 可知该函数的主要作用是将 主机字节序 转化为 网络字节序 ,关于这两个字节序后续再深入研究. 这里可以理解为:htons() 的主要作用就是将十进制的 ip 地址和端口号转化为网络可以识别的 ” 东东 ”.

至此,基本可以完成座机的安装入户和号码绑定:

bing(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

3)、listen 监听函数

对于我们的服务器而言,它需要监听来自客户端发来的消息,Linix 终端中 man listen可以看到详细信息. 函数声明为:

int listen(int socdfd,int backlog);

其中参数 sockfd 代指所要监听的套接字文件描述符,参数 backlog 表示在套接字挂起时,所能接受请求的最大队列长度. 函数执行成功返回 0,否则返回 -1.

必须说明一点,当调用该函数后,参数 socdfd 所指定的套接字将变为 被动套接字, 所谓被动套接字,是指其只能用来接收来自其他用户的链接请求. 类似于改变了套接字的状态,使其只能用于接收.

4)、accept 接收函数

对于我们的服务器而言,由于其只具备接收功能,因此必须创建一个接受函数:

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

函数参数不言自明,参数 1sockfd 表示服务器 socket 描述符,参数 2 是指客户端的协议地址,参数 3 为地址长度. 函数成功返回监听的等待队列中第一个套接字的描述符.

3、服务器实现

服务器的功能是监听客户端发来的消息,并将消息广播给客户端. 因此需要一个循环实时监听客户端发来的消息,在本地构建一个简单的服务器如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define ERR_EXIT(m) \
  do \
  { \
    perror(m);\
    exit(EXIT_FAILURE);\
   }while(0)

int main(void){
  
  int listenfd;
    if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0){ERR_EXIT("socket");
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
      ERR_EXIT("bind");
      
    // 一旦监听,则为被动套接字(只能接受连接,调用 accep 函数之前调用),这里随便给了一个最大队列长度
    if(listen(listenfd,100)< 0)  
      ERR_EXIT("listen");
    
    // 声明一个地址,用于存储客户端链接时的协议地址
    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;  // 返回的一个主动套接字
    if((conn= accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
        ERR_EXIT("accept");

    char recvbuff[1024];
    while(1){memset(recvbuff,0,sizeof(recvbuff));
            int ret = read(conn,recvbuff,sizeof(recvbuff));
            fputs(recvbuff,stdout);
            write(conn,recvbuff,ret);

        }
    close(listenfd);    
  return 0;
}

其中用到了几个非 socket API 的函数:

ssize_t read(int fd,void *buf,size_t count);
ssize_t write(int fd, const void *buf, size_t count);

read()函数:负责从 fd 所指定文件描述符读取字节大小为 count 的数据到 buf 中. 若成功返回实际读取到的字节大小, 否则返回负数, 返回 0 表示读取到文件结束.
write(): 将 buf 中的 count 个字节内容写入文件描述符 fd. 成功时返回写的字节数.

4、客户端实现

客户端的实现和服务器的实现之间大同小异,同样都需要”安装电话“,但是客户端的功能仅在于向外”拨打电话“. 区别在于客户端是主动发起连接请求, 所以它必须知道自己所要连接的目标,之后服务器才有响应. 同样客户端并不需要监听,只需要接收到服务器的广播即可. 发起连接请求需要函数 connect

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

在上述连接函数中,参数 sockfd 表示本机 (客户端) 的 socket 套接字描述符,参数 addr 表示服务器端的地址,参数 3 表示地址长度.
代码实现:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define ERR_EXIT(m) \
  do \
  { \
    perror(m);\
    exit(EXIT_FAILURE);\
   }while(0)

int main(void){
  
  int sock;
    if((sock = socket(PF_INET,SOCK_STREAM,0)) < 0){ERR_EXIT("socket");
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
   // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    // 发起连接
    connect(sock,(struct sockadddr*)&serveraddr,sizeof(serveraddr));
 
    char recvbuf[1024]={0};
    char sendbuf[1024]={0};
    while(fgets(sendbuf,sizeof(sendbuf),stdin)!= NULL){write(sock,sendbuf,strlen(sendbuf));
            read(sock,recvbuf,sizeof(recvbuf));
            fputs(recvbuf,stdout);
            memset(recvbuf,0,sizeof(recvbuf));
        }

     close(sock);
  return 0;
}

上述函数功能就是从客户端主动向服务器发送连接请求,并在客户端机器的标准设备上如字符,服务器接受并返回. 实现两台机器通信的模拟.

5、结果

效果如下图:
gcc 编译上述两个文件,首先启动服务器,之后启动客户端. 在客户端随便输入字符,服务器解收到并广播返回. 至此基本完成目的.

三、总结

目前来看,创建服务器的一般流程是:

1. 创建 socket 套接字(`socket` 函数);2. 创建服务器地址,地址包含协议族、IP 和端口号(`const struct sockaddr*`);3. 绑定套接字和服务器地址(bind 函数);
4. 系统监听服务器, 一旦监听则该套接字变为被动套接字,只能用于接收数据(`listen` 函数);5. 作为服务器,应该能接收客户端信息(`accept` 函数), 该函数返回一个主动套接字;

基于以上步骤,基本能搭建一个简单的服务器.

客户端的搭建相比而言简单许多:

1. 创建用于连接的套接字;2. 将套接字和服务器地址连接;3. 发送消息
网络编程毕竟浪大水深,毕竟初涉,慢慢填充.
退出移动版