TCP/IP和网络编程

本章论述了 TCP/IP和网络编程,分为两个部分。第一部分论述了 TCP/IP协议及其应 用,具体包括TCP/IP栈、IP地址、主机名、DNS、IP数据包和路由器;介绍了 TCP/IP网 络中的UDP和TCP协议、端口号和数据流;阐述了服务器-客户机计算模型和套接字编程 接口;通过使用UDP和TCP套接字的示例演示了网络编程。第一个编程项目可实现一对通 过互联网执行文件操作的TCP服务器-客户机,可让用户定义其他通信协议来可靠地传输 文件内容。
本章的第二部分介绍了 Web和CGI编程,解释了 HTTP编程模型、Web页面和Web浏 览器;展示了如何配置Linux HTTPD服务器来支持用户Web页面、PHP和CG1编程;阐 释了客户机和服务器端动态Web页面;演示了如何使用PHP和CGI创建服务器端动态Web 页面。第二个编程项目可让读者在Linux HTTPD服务器上通过CGI编程实现服务器端动态 Web页面。

TCP/IP 协议

TCP/IP (Comer 1988, 2001; RFC1180 1991 )是互联网的基础。TCP代表传输控制协议。 IP代表互联网协议。目前有两个版本的IP,即IPv4和IPv6o IPv4使用32位地址,IPv6则 使用128位地址。本节围绕IPv4进行讨论,它仍然是目前使用最多的IP版本。TCP/IP的 组织结构分为几个层级,通常称为TCP/IP堆栈

IP主机和IP地址

主机是支持TCP/IP协议的计算机或设备。每个主机由一个32位的IP地址来标识。为 了方便起见,32位的IP地址号通常用点记法表示,实际上,应用程序通常使用主 机名而不是IP地址。在这个意义上说,主机名就等同于IP地址,因为给定其中一个,我们 可以通过DNS (域名系统)(RFC 134 1987; RFC 1035 1987 )服务器找到另一个,它将IP地 址转换为主机名,反之亦然。

IP地址分为两部分,即NetworkID字段和HostID字段。根据划分,1P地址分为A~E 类。

IP协议

ip协议用于在ip主机之间发送/接收数据包。ip尽最大努力运行。】p主机只向接收主 机发送数据包,但它不能保证数据包会被发送到它们的目的地,也不能保证按顺序发送。这 意味着ip并非可靠的协议。必要时,必须在ip层的上面实现可靠性。

IP数据包格式

路由器

ip主机之间可能相距很远。通常不可能从一个主机直接向另一个主机发送数据包。路 由器是接收和转发数据包的特殊ip主机。如果有的话, 一个ip数据包可能会经过许多路由器,或者跳跃到达 某个目的地。

UDP

UDP (用户数据报协议)(RFC 768 1980; Comer 1988 )在IP上运行,用于发送/接收数 据报。与IP类似,UDP不能保证可靠性,但是快速高效.它可用于可靠性不重要的情况。

ping是一个向目标主机发送带时间戳UDP包的应用程序。接收到一个pinging数据包 后,目标主机将带有时间戳的UDP包回送给发送者,让发送者可以计算和显示往返时间。 如果目标主机不存在或宕机,当TTL减小为0时,路由器将会丢弃pinging UDP数据包。 在这种情况下,用户会发现目标主机没有任何响应。用户可以尝试再次ping,或者断定目标 主机宕机。在这种情况下,最好使用UDP,因为不要求可靠性。

TCP

TCP (传输控制协议)是一种面向连接的协议,用于发送/接收数据流。TCP也可在IP 上运行,但它保证了可靠的数据传输。通常,UDP类似于发送邮件的USPS,而TCP类似 于电话连接。

端口编号

在各主机上,多个应用程序(进程)可同时使用TCP/UDP.每个应用程序由三个组成部分唯一标识
应用程序=(主机IP,协议,端口号)
其中,协议是TCP或UDP,端口号是分配给应用程序的唯一无符号短整数。要想使用 UDP或TCP,应用程序(进程)必须先选择或获取一个端口号。前1024个端口号已被预留。 其他端口号可供一般使用。应用程序可以选择一个可用端口号,也可以让操作系统内核分配端口号。

网络和主机字节序

计算机可以使用大端字节序,也可以使用小端字节序。在互联网上,数据始终按网络 序排列,这是大端。在小端机器上,例如基于Intel x86的PC, htons。、htonl。、ntohs()、 ntohl。等库函数,可在主机序和网络序之间转换数据。

TCP/IP网络中的数据流

网络编程

所有Unix/Linux系统都为网络编程提供TCP/IP支持。本节,我们将会阐释用于网络编 程的平台和服务器-客户机计算模型。

套接字编程

在网络编程中,TCP/IP的用户界面是通过一系列C语言库函数和系统调用来实现的,这些函数和系统调用统称为套接字API((Rago1993;Stevens等2004)。为了使用套接字 API,我们需要套接字地址结构,它用于标识服务器和客户机。netdbh和sys/socketh中有套接字地址结构的定义。

struct sockaddr_in {
sa_family_t sin_family; // AF_INET for TCP/IP
in port_t sinport; // port number 
struct in_addr sin_addr; // IP address
};
struct in_addr{ //internet address 
uint32_t s_addr; // IP address in network byte order 
};

在套接字地址结构中,
TCP/IP网络的sin family始终设置为AFINET。 sin port包含按网络字节顺序排列的端口号。
sinaddr是按网络字节顺序排列的主机IP地址。

套接字API

服务器必须创建一个套接字,并将其与包含服务器IP地址和端口号的套接字地址绑定。它可以使用一个固定端口号,或者让操作系统内核选择一个端口号(如果sin_port为0)。为了与服务器通信,客户机必须创建一个套接字。对于UPD套接字,可以将套接字绑定到服务器地址。如果套接字没有绑定到任何特定的服务器,那么它必须在后续的sendto() recyfrom()调用中提供一个包含服务器IP和端口号的套接字地址。下面给出了socket()系统调用,它创建一个套接字并返回一个文件描述符
1、int套接字(int域,int类型int协议)示例:
int udp_sock=socket(AF INETSOCK_DGRAM,0);将会创建一个用于发送/接收UDP数据报的套接字。
int tep_sock= socket(AF_INET,SOCK_STREAM,0);
将会创建一个用于发送/接收数据流的面向连接的TCP套接字。
新创建的套接字没有任何相联地址。它必须与主机地址和端口号绑定,以识别接收主机或发送主机。这通过bind0系统调用来完成。
2、int bind(int sockfd,struct sockaddr *addr,socklen_t addrlen)
bind()系统调用将addr指定的地址分配给文件描述符sockfd所引用的套接字,addrlen指定addr所指向地址结构的大小(以字节为单位)。对于用于联系其他UDP服务器主机的 UDP套接字,必须绑定到客户机地址,允许服务器发回应答。对于用于接收客户机连接的TCP套接字,必须先将其绑定到服务器主机地址。
3、UDP套接字
UDP套接字使用sendto()/recvfrom0)来发送/接收数据报。
ssizetsendto(int sockfd, const void *buf, sizet len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssizet recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklent *addrlen);
sendto()将缓冲区中的len字节数据发送到由destaddr标识的目标主机,该目标主机包含目标主机IP和端口号。recvfrom()从客户机主机接收数据。除了数据之外,它还用客户机的IP和端口号填充srcaddr,从而允许服务器将应答发送回客户机。
4、TCP套接字
在创建套接字并将其绑定到服务器地址之后,TCP服务器使用listen()和accept()来接收来自客户机的连接
int listen(int sockfdint backlog);
listen()将sockfd引用的套接字标记为将用于接收入连接的套接字。backlog参数定义了等待连接的最大队列长度。
int accept(int sockfd,struct sockaddr *addr,socklen t *addrlen);
accept()系统调用与基于连接的套接字一起使用。它提取等待连接队列上的第一个连接请求用于监听套接字sockfd,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符,与客户机主机连接。在执行accept()系统调用时TCP服务器阻塞,直到客户机通过 connect))建立连接。

实践

#include <stdio.h>
#include <arpa/inet.h>//inet_addr() sockaddr_in
#include <string.h>//bzero()
#include <sys/socket.h>//socket
#include <unistd.h>
#include <stdlib.h>//exit()

#define BUFFER_SIZE 1024

int main() {

    char listen_addr_str[] = "0.0.0.0";
    size_t listen_addr = inet_addr(listen_addr_str);
    int port = 8080;
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_size;
    char buffer[BUFFER_SIZE];//缓冲区大小
    int str_length;
    server_socket = socket(PF_INET, SOCK_STREAM, 0);//创建套接字
    bzero(&server_addr, sizeof(server_addr));//初始化
    server_addr.sin_family = INADDR_ANY;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = listen_addr;
    if (bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr)) == -1) {
        printf("绑定失败\n");

        exit(1);

    }

    if (listen(server_socket, 5) == -1) {

        printf("监听失败\n");

        exit(1);

    }

    printf("创建tcp服务器成功\n");

 

 

    fd_set reads,copy_reads;

    int fd_max,fd_num;

    struct timeval timeout;

 

    FD_ZERO(&reads);//初始化清空socket集合

    FD_SET(server_socket,&reads);

    fd_max=server_socket;

 

 

    while (1) {

        copy_reads = reads;

        timeout.tv_sec = 5;

        timeout.tv_usec = 5000;

 

        //无限循环调用select 监视可读事件

        if((fd_num = select(fd_max+1, &copy_reads, 0, 0, &timeout)) == -1) {

            perror("select error");

            break;

        }

        if (fd_num==0){//没有变动的socket

            continue;

        }

 

        for(int i=0;i<fd_max+1;i++){

            if(FD_ISSET(i,&copy_reads)){

                if (i==server_socket){//server_socket变动,代表有新客户端连接

                    addr_size = sizeof(client_addr);

                    client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &addr_size);

                    printf("%d 连接成功\n", client_socket);

                    char msg[] = "恭喜你连接成功";

                    write(client_socket, msg, sizeof(msg));

                    FD_SET(client_socket,&reads);

                    if(fd_max < client_socket){

                        fd_max=client_socket;

                    }

                }else{

                    memset(buffer, 0, sizeof(buffer));

                    str_length = read(i, buffer, BUFFER_SIZE);

                    if (str_length == 0)    //读取数据完毕关闭套接字

                    {

                        close(i);

                        printf("连接已经关闭: %d \n", i);

                        FD_CLR(i, &reads);//从reads中删除相关信息

                    } else {

                        printf("%d 客户端发送数据:%s \n", i, buffer);

                        write(i, buffer, str_length);//将数据发送回客户端

                    }

                }

            }

        }

 

    }

 

    return 0;

}