UNIX网络编程入门——TCP客户/服务器程序详解

前言

最近刚开始看APUE和UNP来学习socket套接字编程,因为网络这方面我还没接触过,要等到下学期才上计算机网络这门课,所以我就找了本教材啃了一两天,也算是入了个门。

至于APUE和UNP这两本书,书是好书,网上也说这书是给进入unix网络编程领域初学者的圣经,这个不可置否,但这个初学者,我认为指的是接受过完整计算机本科教育的研究生初学者,需要具有完整计算机系统,体系结构,网络基础知识。基础没打好就上来啃书反而会适得其反,不过对于我来说也没什么关系,因为基础课也都上得差不多了,而且如果书读得没有什么阻碍的话又能收获到什么呢?

回到这篇文章来,写这篇的目的,一来是梳理一下最近这几天学到的知识,学而不思则罔。二来是希望通过自己的一点小小工作,能够帮助更多的人轻松入门网络编程,建立起对网络编程的基本知识框架。

主要内容

文章的行文方式主要是通过讲解《UNIX网络编程 卷一》(UNP v1)第五章的TCP客户/服务器程序,并以此为出发点来系统的介绍UNIX环境下的网络编程。该程序虽然简单,但是却包含了unix环境下TCP需要用到的大部分函数,通过学习它,我们可以较快的熟悉相关环境。

学习过这本书的朋友可能知道,作者将大部分网络程序都需要的系统头文件及各种常值都包含在unp.h文件,并且如果我们要运行书里的程序,需要先去下载书的源码,编译成静态链接库后再链接使用。这给我们学习源码及探究里面的流程带来了麻烦和不必要的干扰,为此我将TCP客户/服务器程序需要到的函数及常量都提取了出来,放在了myunp.hmyunp.c里面,这样我们要编译运行该程序的时候,就不再需要去进行所谓的“搭建环境”了,这将大大减少不必要的干扰。

面向读者

此文主要面向初学或未接触网络编程的读者,我希望你有计算机网络的基础,最好像我一样正在学unp,没有也关系,我尽量讲得深入浅出一点,这也是对我能力的提升考验。如有错误纰露之处,还请诸位多加包涵,私信或评论区指出。

一、TCP客户/服务器程序直观展现

人类都是视觉动物,如果先讲原理再看实现容易让人无所适从,不知所以然。所以这里我们先编译运行程序,来看一下程序的运行效果。

程序运行环境:Ubuntu 16.04,使用windows的读者建议使用虚拟机或者安装双系统

  1. 将所有代码放到同一文件夹内并打开终端切换至该文件夹,代码详见文章后半部分,总共四个文件,分别为myunp.hmyunp.ctcpserver.ctcpclient.c
  2. 执行gcc tcpserver.c myunp.c -o tcpserver生成服务器程序tcpserver
  3. 执行gcc tcpclient.c myunp.c -o tcpclient生成客户端程序tcpclient
  4. 执行./tcpserver &后台运行服务器程序(&意为后台运行)
  5. 执行./tcpclient 127.0.0.1运行客户端程序连接到服务器(127.0.0.1为本地ip地址)
  6. 此时输入任意字符按回车后将会发送给服务器,服务器会将字符完整的回送给客户端,并在下一行显示出来,因此该服务器也叫echo服务器。
  7. ctrl+c退出客户端,输入kill 4556关闭服务器(4556为服务端程序的进程id,参见下图)

  • 我们首先编译好了两个程序,分别是服务端和客户端程序,然后将这两个程序都在本机上运行,客户端通过tcp协议与服务端连接后发送消息给服务端,服务端简单的将消息回送给客户端。

    二、计算机网络概述

    上面我们展示了两个程序间的通信,虽然为了方便测试他们都是在同一台机子上运行,但我们可以将他们看成是不同地区的两台电脑之间的通信。

    这里限于篇幅我也不想长篇大论各种概念,我们只要知道,在大多数情况下(除了连wifi和数据),两台能够通信的电脑都是物理相连的,他们之间通信的内容转化为电信号等在他们之间物理相连的线路上传输。那他们怎么把电信号,也就是01010这些二进制转化为可以读的内容呢,这就需要双方约定好怎样去转化,也就是协议,目前主要使用的是tcp/ip协议族,这并不是单个协议,而是由许多协议组成的。

    我们需要知道,一个通信过程并不是仅仅使用一个协议,而是需要各种协议同时使用,但是这里我们并不关心这些,计算机网络中有一个很重要的概念就是抽象,底层的设施封装成可以直接使用的器件交付上层,对上层屏蔽了内部细节。这里我们同样要使用抽象方法,将网络连接看成客户端和服务器之间的一条连线,两个程序在这条连线上使用TCP协议进行通讯。

    三、TCP协议简介

    既然我们将复杂的现实连接抽象成了一条简单的连线,那么接下来就是在这条线上进行客户端与服务器的连接。

    TCP建立连接如上图所示,也就是俗称的三次握手,首先 B(服务器) 处于 LISTEN(监听)状态,等待客户的连接请求。

    • A 向 B 发送连接请求报文段。
    • B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段
    • A 收到 B 的连接确认报文段后,还要向 B 发出确认。
    • B 收到 A 的确认后,连接建立。

之所以要进行第三次握手而不是简单的两次握手,主要原因还是现实网络的不可靠性,它不可能保证你发送的任何一条消息都能被对方接收到,当然这是题外话,并不在我们的讨论范围内。

有建立连接自然就有关闭连接,TCP关闭连接称为四次挥手,如上图所示

  • A 发送连接释放报文段。
  • B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
  • 当 B 不再需要连接时,发送连接释放请求报文段。
  • A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
  • B 收到 A 的确认后释放连接。

这里主要是简单介绍一下TCP连接的过程,并不细究其中原理。

四、编码实践

现在我们已经了解了客户端如何使用TCP跟服务器进行连接,是时候开始编写代码了。先说一下代码组成,主要的代码就是服务器程序tcpserver.c和客户端程序tcpclient.c,这两个的代码比较简洁,因为用到的函数大都进行了封装。另外我将这两个文件用到的函数都提取出来放到了myunp.c里面,函数原型和宏以及用到的系统头文件则在myunp.h里。

这里我就结合代码将相关知识介绍一遍。

服务器程序

tcpserver.c
#include "myunp.h" 

int main(int argc, char **argv)
{
    int                 listenfd, connfd;
    pid_t               childpid;
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for ( ; ; ) {
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);

        if ( (childpid = Fork()) == 0) {    /* child process */
            Close(listenfd);    /* close listening socket */
            str_echo(connfd);   /* process the request */
            exit(0);
        }
        Close(connfd);          /* parent closes connected socket */
    }
}

第1行

包含所需要的头文件myunp.h

第5行

声明监听描述符连接描述符

这里我们要先了解类unix系统的一个核心思想,“一切皆文件”,这里的“文件”不仅仅是我们通常所指的文件,在linux和unix中它代表的更为宽泛。目录、字符设备、块设备、 套接字、进程、线程、管道等都被视为是一个“文件”。当我们打开一个文件时,就得到了一个文件描述符(file descriptor),文件描述符通常是非负整数,代表了一个文件的索引,通过这个索引我们就可以去操作文件。

现在我们知道了描述符是用来当做文件的索引,那么上面的监听描述符和连接描述符又是什么呢?别急,我们先认识一下socket,英文原义是“插座”,我们通常把它翻译作“套接字”,socket本质是编程接口(API),对TCP/IP的封装,我们可以先将它简单理解成一个插口,在客户端和服务器两边分别都打开一个socket,然后两个插口相连,这就形成了一条可以通信的线路了。两个socket接口分别返回一个文件描述符,这个描述符就是对方主机的索引,比如客户端通过得到的文件描述符可以读取到服务器写过来的内容。

那么现在就有一个问题了,我们这个服务器程序为什么有两个描述符呢,不是一个就可以跟客户端通信了吗?实际上,上面只是简化了的叙述,现在我们来详细了解socket的工作流程:

  • 客户端和服务器使用socket函数来创建一个套接字描述符(socket descriptor),但这个描述符是半打开的,还不能用于读写。
  • 对于客户端,使用connect函数与服务器建立连接,如果连接成功,套接字描述符将会完全打开,可以进行读写了。
  • 对于服务器,需先使用bind函数套接字描述符与本服务器ip地址和端口绑定,然后使用listen函数套接字描述符转换为一个监听描述符(listening)。这时再使用accept函数等待客户端的连接请求到达监听描述符,一旦接收到来自客户端的连接请求,就返回一个新的已连接描述符(connected descriptor),这个描述符就可以用来与客户端通信了。

如上图,当连接建立后,客户端和服务器就可以进行通信了。很明显,服务器做的工作比客户端多得多,监听描述符和已连接描述符之间的区别也容易让人感到迷惑。这里解释一下,监听描述符只被创建一次,只有一个,它是作为服务器接受请求的一个窗口。而只要服务器在这个窗口每接受一次连接请求,就创建一个已连接描述符,它则是客户端和服务器连接的端点。如下图所示,监听描述符作为一个端口接受来自客户端的连接请求,当服务器允许连接就生成一个已连接描述符,客户端和服务器之间的连接就是建立在这个端点之上。(下面图片的过程对应于上面连接建立的过程)

好了,解释到这里你也应该基本明白了监听描述符连接描述符是干什么的,可能你还会有疑问,为什么就只声明一个已连接描述符,不是说每接受一个连接请求就要生成一个新的已连接描述符吗?这个问题我们先放着。

第6行

声明子进程id,这里类型后缀为_t的都是系统数据类型。

我们这个程序是并发的,意味着能够同时接受多个客户端的请求。这里是使用fork函数来实现的,该函数是UNIX中派生新进程的唯一方法。每次有客户端发生连接请求到来时,该程序都会派生出一个子进程来处理客户端的请求。子进程是父进程的一个副本,也就是好像是把这个服务器程序复制了一份一样,但子进程和父进程里面fork函数的返回值是不一样的,父进程中返回的是子进程的进程id,而子进程里面返回的是0,我们就可以利用这一点,编写一个判别语句,根据fork函数返回值来确定这是哪个进程,要执行什么操作。

如下图所示,在上面连接已建立的连接之上,为了实现并发的能力,我们使用fork函数复制了一个子进程,这个子进程跟父进程的内容是一样的,这就导致了连接形成的分叉,怎么办呢?前面我们不是说过子进程和父进程里面fork函数的返回值是不一样的嘛,我们根据这个返回值作为判别条件,在父进程里面就把connfd(已连接描述符)关闭,在子进程里面就把listenfd(监听描述符)关闭。这样,父进程仍旧作为服务器接受连接请求的窗口等待请求的到来,而子进程就为刚才请求连接的客户端服务,达到了并发的目的。

作为背景知识,这里我们再提一下:父进程和子进程共享相同的内存空间,也就是说他们所引用的描述符是同一个东西,那么为什么子进程关闭了listenfd却不会使父进程的listenfd也被关闭呢?熟悉c++ 11标准的朋友可能已经猜到了,这里的原理跟c++ 智能指针的原理差不多,即采用了引用计数。对于一个描述符,我们使用一个引用计数值来表示有多少个对这个描述符的使用,比如这里的listenfd有父进程和子进程两个引用者,所以他的引用计数值为2,当子进程关闭它的listenfd时,所做的仅仅是将这个引用计数值减1而已,只有当引用计数值变成0时才会真正关闭这个描述符。

第7~8行

声明客户端地址结构长度

声明客户端和服务器套接字地址结构

在unix中,每个协议族都定义了它自己的套接字地址结构,这个结构里面包含ip地址、端口号、所使用的协议等信息。这里的服务器套接字地址结构保存服务器的ip地址和端口等信息,客户端地址结构则在每一次接受连接请求时保存客户端的信息。客户端地址结构长度的意义在于当连接请求到来时,内核要将客户端的信息写入客户端地址结构中,长度限制了内核写入的大小,避免产生越界。

第10行

创建套接字描述符

Socket(AF_INET, SOCK_STREAM, 0)

第一个参数指IPv4协议族,第二个为字节流套接字,第三个为使用的协议(protocol),0的意思是选择前两个参数组合的系统默认值,这里默认是TCP协议。

第12~15行

填充服务器地址结构

bzero函数把整个结构清零,这是一种惯例写法。

而后填写所使用的协议族、ip地址及端口。这里之所以使用servaddr.sin_addr.s_addr是因为sin_addr是一个结构(struct),而不是一个简单的字段,具体的ip地址字段在这个结构的s_addr中,这是因为一些历史上的原因(关于地址分类),这里我们也不深究了。

后面使用的htonlhtons函数意为Host to Network LongHost to Network Short,分别是将一个长整数或短整数从主机字节顺序转换成网络字节顺序。

字节顺序指的是一个字节存放在内存中的顺序,主要有大端法和小端法。网络字节顺序一般是大端顺序,而主机字节顺序则依赖于具体的操作系统和cpu芯片。学过汇编的同学应该就清楚,我们内存中一个单元为一个字节,有8bit,那么这个单元就可以存放用十六进制表示的两个数了,比如一个内存单元,可以存放16进制的0x12,也就是二进制的0001 0010。

如下图所示,一个16进制数0x01234567存放在一段连续的内存单元中,如果是大端法,存放的顺序就是01 23 45 67,而小端法就是67 45 23 01。

因为不同的字节顺序不能进行通信,所以必须进行转换。其中的参数INADDR_ANY表示通配地址,一些服务器可能会有多个网络接口,因而有了多个ip地址,通配地址表示访问任一个ip地址都能访问到这个程序。参数SERV_PORT定义在myunp.h中,值为9877。

第17行

将套接字描述符与服务器地址结构进行绑定

这里第二个参数(SA *) &servaddr把servaddr强制转换为SA类型,SA为struct sockaddr的缩写,定义在myunp.h中,仅仅是为了方便书写而已,而sockaddr是通用地址结构,为什么要强转为通用地址结构呢?因为不同的协议族有不同的套接字地址结构,但bind函数要求必须要有一个确切的参数类型,所以就将参数类型设置为struct sockaddr,需要使用bind函数时就将具体的结构类型强转一下就行。(额外提一下,之所以采用这种方法是由于历史所限,如今已经有了通用的指针类型void *了。)

细心的朋友可能已经注意到了,这里代码写的是Bind而不是bind,这是作者的一种编程风格,也称做包裹函数,因为bind函数会有时会返回一些错误,如果将错误处理代码都写在主程序里面就显得很繁杂,因此作者就将这些错误处理和原函数写在一个函数里面,使用时调用这个外层的函数就行了。后面还有许多包裹函数,它们统一的特征就是函数名首字母大写,这些函数的定义都在myunp.c中。

第19行

将套接字描述符转换为一个监听描述符

第二个参数为监听队列的最大排队数,具体实现原理有些复杂,这里不去详解,LISTENQ值为1024,定义在myunp.h中。

第21~31行

等待来自客户端的连接请求,当空闲时就阻塞在Accept函数处,一旦有请求到了就fork出一个子进程,25~29行为子进程的处理内容,它先关闭listenfd,接着调用str_echo直接把客户端发送过来的内容回送回去,处理完了就退出子进程。30行为父进程的内容,它仅仅简单的关闭connfd然后就继续等待连接请求了,周而复始。注意,上述两个过程是同时发生的,父子进程的执行是分开的。

客户端程序

tcpclient.c
#include    "myunp.h"

int
main(int argc, char **argv)
{
    int                 sockfd;
    struct sockaddr_in  servaddr;

    if (argc != 2)
        err_quit("usage: tcpcli <IPaddress>");

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

    str_cli(stdin, sockfd);     /* do it all */

    exit(0);
}

第6~7行

声明套接字描述符和服务器地址结构

第9~10行

验证命令行参数数量,我们执行客户端程序时要使用命令./tcpclient.c 127.0.0.1,这里面就是两个参数了,第一个参数是程序的名字,第二个就是后面的127.0.0.1,当参数数量不正确时,就会报错并退出。这里的err_quit函数也是包裹函数,它输出错误信息并终止程序。

第12行

生成套接字描述符

第14~17行

填写服务器地址结构信息

其中Inet_pton函数把服务器地址从表达(presentation)形式转换为数值(numeric)形式,并保存在servaddr.sin_addr中,比如把127.0.0.1转换为01111111 00000000 00000000 00000001。它同样也包裹函数,具体实现比较复杂,因为它既可以用于IP v4地址也可以用于IP v6地址。

第19行

连接到服务器

第12行

该函数将用户在命令行中输入的字符串发送给服务器,而后将收到的来自服务器的消息显示在屏幕上。



到这里服务器和客户端程序基本讲解完毕,注意这只是一个简单的程序,缺乏应对各种极端情况的能力,但这其中的原理已经足够我们一探网络编程的世界了。剩下的就是一些提取出来的函数和定义了,这里我就直接贴代码,不再单独讲了,对程序实现底层细节有兴趣的朋友可以研读下下面的代码。

myunp.h
#ifndef MY_UNP_H
#define MY_UNP_H

#include    <sys/socket.h>  /* basic socket definitions */
#include    <netinet/in.h>  /* sockaddr_in{} and other Internet defns */
#include    <string.h>      /* bzero(); */
#include    <stdlib.h>      /* exit(); */
#include    <stdio.h>       /* snprintf(); */
#include    <errno.h>
#include    <unistd.h>
#include    <stdarg.h>      /* ANSI C header file */
#include    <syslog.h>      /* for syslog() */
#define SERV_PORT   9877    /* TCP and UDP */
#define LISTENQ     1024    /* 2nd argument to listen() */
#define MAXLINE     4096    /* max text line length */

#ifndef AF_INET6
#define AF_INET6    AF_MAX+1    /* just to let this compile */
#endif
/* Following shortens all the typecasts of pointer arguments: */
#define SA          struct sockaddr
#define IN6ADDRSZ   16
#define INADDRSZ     4
#define INT16SZ      2

/* 套接字连接相关函数 */
int      Socket(int, int, int);
void     Bind(int, const SA *, socklen_t);
void     Listen(int, int);
int      Accept(int, SA *, socklen_t *);
void     Connect(int, const SA *, socklen_t);
void     Close(int);

/* 派生进程相关 */
pid_t    Fork(void);

/* 发送、回送相关 */
void     str_echo(int);
void     str_cli(FILE *, int);

/* 错误处理相关 */
void     err_sys(const char *, ...);
void     err_quit(const char *, ...);

/* 字符串输入输出相关 */
void     Writen(int, void *, size_t);
char    *Fgets(char *, int, FILE *);
void     Fputs(const char *, FILE *);
ssize_t  Readline(int, void *, size_t);

/* 地址转换相关 */
void     Inet_pton(int, const char *, void *);

#endif /* MY_UNP_H */
myunp.c
#include "myunp.h" /* for implicit declaration of function ‘socket’ */

/* 套接字连接相关函数 */

int Socket(int family, int type, int protocol)
{
    int     n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (bind(fd, sa, salen) < 0)
        err_sys("bind error");
}

void Listen(int fd, int backlog)
{
    char    *ptr;
    /*4can override 2nd argument with environment variable */
    if ( (ptr = getenv("LISTENQ")) != NULL)
        backlog = atoi(ptr);

    if (listen(fd, backlog) < 0)
        err_sys("listen error");
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
    int     n;
again:
    if ( (n = accept(fd, sa, salenptr)) < 0) {
#ifdef  EPROTO
        if (errno == EPROTO || errno == ECONNABORTED)
#else
        if (errno == ECONNABORTED)
#endif
            goto again;
        else
            err_sys("accept error");
    }
    return(n);
}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (connect(fd, sa, salen) < 0)
        err_sys("connect error");
}

void Close(int fd)
{
    if (close(fd) == -1)
        err_sys("close error");
}

/* 派生进程相关 */

pid_t Fork(void)
{
    pid_t   pid;
    if ( (pid = fork()) == -1)
        err_sys("fork error");
    return(pid);
}

/* 发送、回送相关 */

void str_echo(int sockfd)
{
    ssize_t     n;
    char        buf[MAXLINE];
again:
    while ( (n = read(sockfd, buf, MAXLINE)) > 0)
        Writen(sockfd, buf, n);

    if (n < 0 && errno == EINTR)
        goto again;
    else if (n < 0)
        err_sys("str_echo: read error");
}

void str_cli(FILE *fp, int sockfd)
{
    char    sendline[MAXLINE], recvline[MAXLINE];
    while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Writen(sockfd, sendline, strlen(sendline));

        if (Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");

        Fputs(recvline, stdout);
    }
}

/* 错误处理相关 */

int     daemon_proc;        /* set nonzero by daemon_init() */
static void err_doit(int, int, const char *, va_list);

void err_sys(const char *fmt, ...)
{
    va_list     ap;
    va_start(ap, fmt);
    err_doit(1, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1);
}

void err_quit(const char *fmt, ...)
{
    va_list     ap;
    va_start(ap, fmt);
    err_doit(0, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1);
}

static void err_doit(int errnoflag, int level, const char *fmt, va_list ap)
{
    int     errno_save, n;
    char    buf[MAXLINE + 1];

    errno_save = errno;     /* value caller might want printed */
#ifdef  HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);   /* safe */
#else
    vsprintf(buf, fmt, ap);                 /* not safe */
#endif
    n = strlen(buf);
    if (errnoflag)
        snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
    strcat(buf, "\n");

    if (daemon_proc) {
        syslog(level, buf);
    } else {
        fflush(stdout);     /* in case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }
    return;
}

/* 字符串输入输出相关 */

ssize_t writen(int fd, const void *vptr, size_t n)/* Write "n" bytes to a descriptor. */
{
    size_t      nleft;
    ssize_t     nwritten;
    const char  *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;       /* and call write() again */
            else
                return(-1);         /* error */
        }

        nleft -= nwritten;
        ptr   += nwritten;
    }
    return(n);
}

void Writen(int fd, void *ptr, size_t nbytes)
{
    if (writen(fd, ptr, nbytes) != nbytes)
        err_sys("writen error");
}

char * Fgets(char *ptr, int n, FILE *stream)
{
    char    *rptr;

    if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream))
        err_sys("fgets error");

    return (rptr);
}

void Fputs(const char *ptr, FILE *stream)
{
    if (fputs(ptr, stream) == EOF)
        err_sys("fputs error");
}

static int  read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];

static ssize_t
my_read(int fd, char *ptr)
{

    if (read_cnt <= 0) {
again:
        if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR)
                goto again;
            return(-1);
        } else if (read_cnt == 0)
            return(0);
        read_ptr = read_buf;
    }

    read_cnt--;
    *ptr = *read_ptr++;
    return(1);
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char    c, *ptr;

    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ( (rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == ‘\n‘)
                break;  /* newline is stored, like fgets() */
        } else if (rc == 0) {
            *ptr = 0;
            return(n - 1);  /* EOF, n - 1 bytes were read */
        } else
            return(-1);     /* error, errno set by read() */
    }

    *ptr = 0;   /* null terminate like fgets() */
    return(n);
}

ssize_t readlinebuf(void **vptrptr)
{
    if (read_cnt)
        *vptrptr = read_ptr;
    return(read_cnt);
}

ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
    ssize_t     n;

    if ( (n = readline(fd, ptr, maxlen)) < 0)
        err_sys("readline error");
    return(n);
}

/* 地址转换相关 */

static int  inet_pton4(const char *src, u_char *dst);
static int  inet_pton6(const char *src, u_char *dst);

int inet_pton(af, src, dst)
    int af;
    const char *src;
    void *dst;
{
    switch (af) {
    case AF_INET:
        return (inet_pton4(src, dst));
    case AF_INET6:
        return (inet_pton6(src, dst));
    default:
        errno = EAFNOSUPPORT;
        return (-1);
    }
    /* NOTREACHED */
}

static int inet_pton4(src, dst)
    const char *src;
    u_char *dst;
{
    static const char digits[] = "0123456789";
    int saw_digit, octets, ch;
    u_char tmp[INADDRSZ], *tp;

    saw_digit = 0;
    octets = 0;
    *(tp = tmp) = 0;
    while ((ch = *src++) != ‘\0‘) {
        const char *pch;

        if ((pch = strchr(digits, ch)) != NULL) {
            u_int new = *tp * 10 + (pch - digits);

            if (new > 255)
                return (0);
            *tp = new;
            if (! saw_digit) {
                if (++octets > 4)
                    return (0);
                saw_digit = 1;
            }
        } else if (ch == ‘.‘ && saw_digit) {
            if (octets == 4)
                return (0);
            *++tp = 0;
            saw_digit = 0;
        } else
            return (0);
    }
    if (octets < 4)
        return (0);
    /* bcopy(tmp, dst, INADDRSZ); */
    memcpy(dst, tmp, INADDRSZ);
    return (1);
}

static int inet_pton6(src, dst)
    const char *src;
    u_char *dst;
{
    static const char xdigits_l[] = "0123456789abcdef",
              xdigits_u[] = "0123456789ABCDEF";
    u_char tmp[IN6ADDRSZ], *tp, *endp, *colonp;
    const char *xdigits, *curtok;
    int ch, saw_xdigit;
    u_int val;

    memset((tp = tmp), 0, IN6ADDRSZ);
    endp = tp + IN6ADDRSZ;
    colonp = NULL;
    /* Leading :: requires some special handling. */
    if (*src == ‘:‘)
        if (*++src != ‘:‘)
            return (0);
    curtok = src;
    saw_xdigit = 0;
    val = 0;
    while ((ch = *src++) != ‘\0‘) {
        const char *pch;

        if ((pch = strchr((xdigits = xdigits_l), ch)) == NULL)
            pch = strchr((xdigits = xdigits_u), ch);
        if (pch != NULL) {
            val <<= 4;
            val |= (pch - xdigits);
            if (val > 0xffff)
                return (0);
            saw_xdigit = 1;
            continue;
        }
        if (ch == ‘:‘) {
            curtok = src;
            if (!saw_xdigit) {
                if (colonp)
                    return (0);
                colonp = tp;
                continue;
            }
            if (tp + INT16SZ > endp)
                return (0);
            *tp++ = (u_char) (val >> 8) & 0xff;
            *tp++ = (u_char) val & 0xff;
            saw_xdigit = 0;
            val = 0;
            continue;
        }
        if (ch == ‘.‘ && ((tp + INADDRSZ) <= endp) &&
            inet_pton4(curtok, tp) > 0) {
            tp += INADDRSZ;
            saw_xdigit = 0;
            break;  /* ‘\0‘ was seen by inet_pton4(). */
        }
        return (0);
    }
    if (saw_xdigit) {
        if (tp + INT16SZ > endp)
            return (0);
        *tp++ = (u_char) (val >> 8) & 0xff;
        *tp++ = (u_char) val & 0xff;
    }
    if (colonp != NULL) {
        /*
         * Since some memmove()‘s erroneously fail to handle
         * overlapping regions, we‘ll do the shift by hand.
         */
        const int n = tp - colonp;
        int i;

        for (i = 1; i <= n; i++) {
            endp[- i] = colonp[n - i];
            colonp[n - i] = 0;
        }
        tp = endp;
    }
    if (tp != endp)
        return (0);
    /* bcopy(tmp, dst, IN6ADDRSZ); */
    memcpy(dst, tmp, IN6ADDRSZ);
    return (1);
}

void Inet_pton(int family, const char *strptr, void *addrptr)
{
    int     n;
    if ( (n = inet_pton(family, strptr, addrptr)) < 0)
        err_sys("inet_pton error for %s", strptr);  /* errno set */
    else if (n == 0)
        err_quit("inet_pton error for %s", strptr); /* errno not set */
    /* nothing to return */
}

原文地址:https://www.cnblogs.com/silence1772/p/9350888.html

时间: 2024-10-17 21:18:58

UNIX网络编程入门——TCP客户/服务器程序详解的相关文章

【UNIX网络编程】TCP客户/服务器程序示例

做一个简单的回射服务器: 客户从标准输入读入一行文本,写给服务器 -> 服务器从网络输入读入这行文本,并回射给客户 -> 客户从网络输入读入这行回射文本,并显示在标准输出上 以下是我的代码(部分.h文件是由unpv13e文件夹中的.c文件改名得到) #include "../unpv13e/unp.h" #include "../unpv13e/apueerror.h" #include "../unpv13e/wrapsock.h"

unix网络编程各种TCP客户-服务器程序设计实例(二)

本节我们接着介绍另外的几种TCP客户-服务器程序: 第四种:TCP并发服务器,每个客户一个子线程 在我们前面的并发服务器程序例子中可以看出:父进程接受连接,派生子进程,子进程处理与客户的交互. 这种模式的问题: fork()是昂贵的.内存映像要从父进程拷贝到子进程,所有描述字要在子进程中复制等等. fork()子进程后,需要用进程间通信在父子进程之间传递信息. 一个进程中的所有线程共享相同的全局内存,这使得线程很容易共享信息,但是这种简易型也带来了同步问题.一个进程中的所有线程不仅共享全局变量,

unix网络编程各种TCP客户-服务器程序设计实例附环境搭建和编译方法(一)

一,到http://download.csdn.net/detail/ts173383201/4505201去下载源代码,然后解压: 二,cd到你解压后的文件夹下,就是有configure的那个目录下,执行命令./configure: 三,执行cd lib跳到lib目录下,执行make命令,会在上层目录(就是刚才有configure那个目录)生成libunp.a文件 四,复制这个静态库libunp.a到/usr/lib/和/usr/lib64/中; 五,接下来在目录中找到unp.h和config

unix网络编程各种TCP客户-服务器程序设计实例(三)

第五种  TCP预先派生子进程服务器程序: 对预先派生子进程服务器的最后一种改动就是由父进程调用accept,然后再将所接受的已连接描述字传递给子进程.父进程必须跟踪子进程的忙闲状态,以便给空闲子进程传递新的描述字.为每个子进程维护一个信息结构,用来管理各子进程. 在调用fork之前,先创建一个字节流管道(Unix域的字节流套接口),它是Unix域的字节流套接口.当子进程派生后,父进程关闭一个描述字(sockfd[1]),子进程关闭另一个描述字(sockfd[0]),此外,子进程将流管道的字节所

《UNIX网络编程》TCP客户端服务器:并发、消息回显

经过小小改动,把前面基础的例子做出一点修改. 并发服务器,服务器每accept一个请求就fork()一个新的子进程. 编译运行方法同前一篇. /*client_tcp.c*/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #incl

UNIX网络编程入门——I/O复用

UNIX网络编程入门--TCP客户/服务器程序详解 UNIX网络编程入门--TCP客户/服务器程序存在问题及解决 在介绍I/O复用之前,我们先来看一个情况:运行我们前面两篇文章里面的服务器和客户端程序,当客户端在等待用户输入一行字符时,服务器崩溃或者关机了.此时虽然服务器TCP会正确地发送FIN给客户端TCP,但客户端阻塞于fget函数,等待从标准输入读入,无法及时地知道服务器已经终止,要等到它得到标准输入发送给服务器时才会返回错误. 要解决这个问题,就需要一种能力,能够同时观察多个I/O条件是

【UNIX网络编程(三)】TCP客户/服务器程序示例

上一节给出了TCP网络编程的函数,这一节使用那些基本函数编写一个完成的TCP客户/服务器程序示例. 该例子执行的步骤如下: 1.客户从标准输入读入一行文本,并写给服务器. 2.服务器从网络输入读入这行文本,并回射给客户. 3.客户从网络输入读入这行回射文本,并显示在标准输出上. 用图描述如下: 编写TCP回射服务器程序如下: #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <st

UNIX网络编程笔记(4)—TCP客户/服务器程序示例

TCP客户/服务器程序示例 这一章信息量开始大起来了,粗略来看它实现了简单的TCP客户/服务器程序,里面也有一些费解的细节. 1.概述 完整的TCP客户/服务器程序示例.这个简单的例子将执行如下步骤的一个回射服务器(这里的回射服务器就是服务简单的把客户端发送的消息返回给客户): 1)客户从标准输入读入一行文本,并写给服务器 2)服务器从网络输入读入这行文本,并回射给客户 3)客户从网络输入读入这行回射文本,并显示在标准输出上 这样实际上就构成了一个全双工的TCP连接. 本章就围绕了这个简单的TC

Java网络编程(tcp在服务器上应用多线程)

package org.tcp; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; public class EchoThread implements Runnable { private Socket client = null; public EchoThread(Socket client){ this.c