UNIX网络编程-简介

发布于:2024-10-18 ⋅ 阅读:(10) ⋅ 点赞:(0)

概述


要编写通过计算机网络通信的程序,首先要确定这些程序相互通信所用的协议(protocal)。

大多数网络应用划分为客户端(client)和服务器(server)。在设计网络应用时,确定总是由客户发起请求往往能够简化协议和程序本身。当然一些较为复杂的网络应用还需要异步回调通信,也就是由服务器向客户端发起请求。

通常客户端每次只与一个服务器通信,但一个服务器可以同时与多个客户端通信。一个服务器同时处理多个客户端请求:

可认为客户端与服务器之间是通过某个网络协议通信的,但实际上这样的通信通常涉及多个网络协议层。本书的焦点是TCP/IP协议族,也称为网际协议族。

客户端和服务器处于同一局域网:

Web客户端与服务器之间使用TCP通信,TCP又转而使用IP通信,IP再通过某种形式的数据链路层通信。如果客户端与服务器处于同一个以太网(以太网是一种局域网技术),则通信层次如下:

注意:

  1. 客户端与服务器之间的信息流在其中一端是向下通过协议栈的,跨越网络后,在另一端则是向上通过协议栈的。
  2. 客户端与服务器通常是用户进程,而TCP和IP协议通常是内核中协议栈的一部分。
  3. 上述所说的IP协议是指IPv4(自20世纪80年代早期以来一直在使用),在20世纪90年代中期开发了IPv6,将来可能会取代IPv4。

客户端和服务器处于不同局域网:

如果客户端和服务端处于两个局域网(LAN)下,则需要使用路由器把这两个局域网连接到广域网(WAN)中。

注意:

路由器是广域网的架构设备,当今最大的广域网是因特网。许多公司也构建自己的广域网,而这些私用的广域网既可以连接到因特网,也可以不连接因特网。

时间获取客户端程序


IPv4协议相关

下述代码是从服务器获取时间的客户端程序,该客户端与服务器建立TCP连接后,服务器以直观可读格式返回当前时间和日期。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */

#define MAXLINE     4096
#define SA  struct sockaddr    

int main(int argc, char **argv)
{
    int                 sockfd, n;
    char                recvline[MAXLINE + 1];
    struct sockaddr_in  servaddr;

    if (argc != 2)
        exit(1);
    
    /* --------------------------------------------- */
    //1) 创建一个TCP连接套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        printf("socket error");
        return -1;
    }
    
    /* --------------------------------------------- */
    //2) 指定服务器的IP地址和端口
    bzero(&servaddr, sizeof(servaddr));         // 初始化内存
    servaddr.sin_family = AF_INET;              // 地址族
    servaddr.sin_port   = htons(13);            // 时间获取服务器端口为13
    // 注意:此处的IP和端口是服务器的IP和端口
    // 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        printf("inet_pton error for %s", argv[1]);
        return -1;
    }
    
    /* --------------------------------------------- */
    //3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接
    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
        printf("connect error");
        return -1;
    }
        
    /* --------------------------------------------- */
    //4) 读取服务器应答
    // read读取服务器应答,如果数据量较大,不能确保一次read调用能返回服务器整个应答
    // 因此,通常需要把read放到一个循环中
    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    // 读取数据后,在末尾加空字符来确保字符数组合法
        // 输出结果
        if (fputs(recvline, stdout) == EOF) {
            printf("fputs error");
            return -1;
        }
    }
    if (n < 0)
        printf("fputs error");
    
    /* --------------------------------------------- */
    //5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字
    exit(0);
}

注意:上述获取时间的客户端代码是与IPv4协议相关的!

IPv6协议相关

#include    "unp.h"

int
main(int argc, char **argv)
{
    int                 sockfd, n;
    char                recvline[MAXLINE + 1];
    
    // 第一处修改
    struct sockaddr_in6 servaddr;

    if (argc != 2)
        err_quit("usage: a.out <IPaddress>");
    
    // 第二处修改
    if ( (sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0)
        err_sys("socket error");

    bzero(&servaddr, sizeof(servaddr));
    
    // 第三处修改
    servaddr.sin6_family = AF_INET6;
    
    // 第四处修改
    servaddr.sin6_port   = htons(13);   /* daytime server */
    
    // 第五处修改
    if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0)
        err_quit("inet_pton error for %s", argv[1]);

    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
        err_sys("connect error");

    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
        if (fputs(recvline, stdout) == EOF)
            err_sys("fputs error");
    }
    if (n < 0)
        err_sys("read error");

    exit(0);
}

注意:

  1. IPv4协议相关和IPv6协议相关客户端代码,只有五处修改!
  2. 上述两个程序,用户必须以点分十进制格式给出服务器的IP地址,如206.168.112.96!

时间获取服务器程序


TCP时间获取服务器程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <arpa/inet.h>
#include <arpa/inet.h>

#define MAXLINE     4096
#define LISTENQ     1024
#define SA  struct sockaddr

int main(int argc, char **argv)
{
    int                 listenfd, connfd;
    struct sockaddr_in  servaddr;
    char                buff[MAXLINE];
    time_t              ticks;
    
    /* --------------------------------------------- */
    //1) 创建一个TCP连接套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        printf("socket error");
        return -1;
    }
    
    /* --------------------------------------------- */
    //2) 把服务器对应端口绑定到套接字 
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;     // 地址族
    // 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(13);   // 时间获取服务器端口为13

    if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
        printf("bind error");
        return -1;
    }
    
    /* --------------------------------------------- */
    //3) 把套接字转换为监听套接字
    // LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数
    if(listen(listenfd, LISTENQ) < 0) {
        printf("listen error");
        return -1;
    }

    /* --------------------------------------------- */
    //4) 接受客户端连接,发送应答
    for ( ; ; ) {
        // connfd为已连接描述符,用于和客户端进行通信
        connfd = accept(listenfd, (SA *) NULL, NULL);
        if(connfd < 0) {
            printf("accept error");
            return -1;
        }
        
        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        
        /* --------------------------------------------- */
        //5) 把结果(时间)写给客户端
        if (write(connfd, buff, strlen(buff)) != strlen(buff)) {
            printf("write error");
            return -1;
        }
        
        /* --------------------------------------------- */
        //6) 关闭与客户端的连接
        if (close(connfd) == -1) {
            printf("close error");
            return -1;
        }
    }
}

问:使用snprintf函数代替sprintf函数有哪些好处?

答:sprintf函数无法检查缓冲区是否溢出,而snprintf要求第二个参数指定目的缓冲区的大小,因此可以确保该缓冲区不溢出。

注:

  1. 许多网络入侵是由黑客通过发送数据,导致服务器对sprintf的调用使其缓冲区溢出而发生的。
  2. 使用fgets、strncat、strncpy代替gets、strcat、strcpy也是同样道理。

 测试

// 编译
gcc -o daytimetcpcli daytimetcpcli.c
gcc -o daytimetcpsrv daytimetcpsrv.c

// 启动服务端程序
./daytimetcpsrv

// 启动客户端程序,同时需要加上ip、port等参数
./daytimetcpcli 127.0.0.1
结果:Thu Aug 22 20:02:59 2024

// 连接服务端方式二
telnet 127.0.0.1 13

OSI模型


描述一个网络中各个协议层的常用方法是使用国际标准化组织(International Organization for Standardization,IOS)的计算机通信开发系统互连(open systems interconnection,OSI)模型。

这是一个七层模型,如图所示,图中还给出了它与国际协议族的近似映射:

 各层含义如下:

  1. 物理层和数据链路层:OSI模型的物理层和数据链路层是随系统提供的设备驱动程序和网络硬件。通常情况下,除需知道数据链路层的某些特性(如1500字节以太网的MTU大小),不必关心这两层的情况。
  2. 网络层:网络层由IPv4和IPv6这两个协议处理。
  3. 传输层:传输层通常由TCP和UDP协议处理,上图中TCP和UDP之间留有间隙,表明网络应用绕过传输层直接使用IPv4或IPv6是有可能的。这就是所谓的原始套接字。
  4. 会话层、表示层和应用层:这三层合称应用层。这就是Web客户(浏览器)、Telnet客户、Web服务器、FTP服务器和其他我们在使用网络应用所在的层。对于网际协议,OSI模型的顶上三层协议几乎没有区别。

问:为什么套接字提供的是从OSI模型的顶上三层进入传输层的接口?

答:有以下两个理由:

  1. 顶上三层处理具体网络应用(如FTP、Telnet或HTTP)的所有细节,却对通信细节了解很少;底下四层对具体网络应用了解不多,却处理所有的通信细节,如发送数据、等待确认,给无序到达的数据排序、计算并验证校验和等等。
  2. 顶上三层通常构成所谓的用户进程,底下四层却通常作为操作系统内核的一部分提供。

Unix与其他现代操作系统都提供分隔用户进程与内核的机制。由此可见,第4层和第5层之间的接口是构建API的自然位置。