TCP客户/服务器

《UNIX Network Programming Volume1: The Socket Networking API, Third Edition》
W.Richard Stevens / Bill Fenner / Andrew M.Rudoff

字节排序函数

#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);

h代表host,n代表network,s代表short(16位值),l代表long(32位值)。事实上,即使在64位主机中,尽管long整型占用64位,htonl和ntohl函数操作的仍然是32位的值。

地址转换函数

#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

这两个函数为IP地址转换函数,在ASCII字符串与网络字节序的二进制值之间转换网际协议。它们是随IPv6出现的,对于IPv4和IPv6地址都适用。函数名中的p和n分别代表表达(presentation)数值(numeric)inet_ntop从数值格式转换到表达格式,len为目标存储单元的大小。为有助于指定这个大小,在<netinet/in.h>中定义了:#define INET_ADDRSTRLEN 16#define INET6_ADDRSTRLEN 46

套接字函数

#include <sys/socket.h>
int socket(int family, int type, int protocol);

family:协议族。如AF_INET(IPv4)/AP_INET6(IPv6)等。
type:套接字类型。如SOCK_STREAM(字节流)/SOCK_DGRAM(数据报)/SOCK_RAW(原始)等。
protocol:协议类型。如IPPROTO_CP/IPPROTO_UDP等。(如设为0,用以选择family与type组合的系统默认值)

2

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

此函数将激发TCP的三路握手过程,且仅在连接建立成功或出错时才返回,出错情况如下:

  1. 调用connect函数时,4.4BSD内核发送一个SYN分节,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个。若总共等了75s后仍未收到响应则返回ETIMEDOUT错误。
  2. 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在指定的端口上没有进程在等待与之连接。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
  3. 若客户发出的SYN在中间的某个路由器上引发了一个”destination unreachable(目的地不可达)“ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该错误消息后按”1“中所述时间间隔继续发送SYN,75s后仍无响应则返回EHOSTUNREACHENETUNREACH错误。
  4. 若connect失败则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数。当循环调用connect为给定主机尝试IP地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket。

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

此函数把一个本地协议地址(IP和端口号)赋予一个套接字。

  • 如果一个TCP客户或服务器未曾调用bind捆绑一个端口(如RPC远程过程调用)或指定端口为0(对于IPv4,INADDR_ANY值一般为0),当调用connect或listen时,内核要为套接字选择一个临时端口。为了得到内核所选择的临时端口值,必须调用函数getsockname来返回协议地址。
  • TCP客户捆绑IP地址到套接字,表示为发送的IP数据报指派了源IP地址(通常不这样做)。TCP服务器捆绑IP地址到套接字,就限定了该套接字只接收那些目的地为这个IP地址的客户连接。
  • bind函数返回的一个常见错误是EADDRINUSE(”Address already in use“)

int listen(int sockfd, int backlog);

此函数仅由TCP服务器调用,将一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。内核为任何一个给定的监听套接字维护两个队列:未完成连接队列已完成连接队列。关于这两个队列,以下几点需要考虑:

  • listen的backlog参数曾被规定为这两个队列总和的最大值,而源自Berkeley的实现给backlog增设了一个模糊因子:把它乘以1.5得到未处理队列最大长度。不要把backlog设为0,因为不同的实现对此有不同的解释。历来沿用的样例代码总将backlog设置为5(实际上允许最多8项在排队)。另外,指定一个比内核能够支持的值还要大的多的backlog也是可接受的,因为内核可把这个偏大值截成自身支持的最大值,而不返回错误。
  • 当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节(不发送RST),等待TCP的正常重传机制重传SYN。

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

此函数由TCP服务器调用,用于从已完成连接队列队头(三路握手完成时,TCP在未完成队列创建的新项会移到已完成连接队列的队尾)返回下一个已完成连接。如果已完成连接队列为空,那么进程将被投入睡眠(套接字为阻塞方式时)。参数cliaddraddrlen用来返回已连接的对端客户的协议地址。 sockfd为监听套接字(listening socket)描述符,它在该服务器的生命期内一直存在。accept的返回值为已连接套接字(connected socket)描述符,当服务器完成对某个客户的服务时,相应的已连接套接字就被关闭。

int close(int sockfd);
int shutdown(int sockfd, int howto);
  • close将相应描述符的引用计数值减1,仅在该计数变为0时才关闭套接字(终止读和写两个方向的数据传送)。
  • shutdown可以不管引用计数就激发TCP的正常连接终止序列。其行为依赖于howto参数:

    • SHUT_RD:关闭连接的读一半。套接字不再接收数据并将接收缓冲区中的数据丢弃。
    • SHUT_WR:关闭连接的写一半。对于TCP,这称为半关闭(half close)。套接字发送缓冲区中的现有数据会被发送掉,后跟TCP的正常终止序列。
    • SHUT_RDWR:等效于第一次调用SHUT_RD,第二次调用SHUT_WR

    int getsockname(itn sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
    int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

这两个函数返回与某个套接字关联的本地协议地址(getsockname)或外地协议地址(getpeername)。

read和write

字节流套接字(TCP)上的read和write函数所表现的行为不同于通常的文件I/O。字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。此时所需的是调用者再次调用read或write函数,以输入或输出剩余的字节。当套接字标志为阻塞时,对于read调用,如果TCP接收缓冲区没有数据read函数就会阻塞住,如果接收缓冲区中有20字节,请求读100个字节,read就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回,如果write中得套接字标志为非阻塞,则直接返回20。
当进程调用read且没有数据返回时,如果套接字的so_error变量为非0值,那么read返回-1且errno被置为so_error的值,随后so_error被复位为0。如果该套接字上有数据在排队等待读取,那么read返回那些数据而不是返回错误条件。如果在进程调用write时套接字的so_error变量为非0值,那么write返回-1且errno被置为so_error的值,随后so_error被复位为0。

简单的TCP客户/服务器

daytimetcpsrv.c:对于时间获取服务,端口号一般为13。由服务器关闭连接表征记录的结束,HTTP 1.0版本也采用这种技术。

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

#define MAXLINE 32
#define error_exit(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr; // 网际套接字地址结构
    char buff[MAXLINE];
    time_t ticks;

    // 创建网际(AF_INET)字节流(SOCK_STREAM)套接字(TCP)
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        error_exit("socket"); 

    // 绑定端口到上述套接字
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET; // 地址族
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY指定IP地址为0.0.0.0(所有地址)
    servaddr.sin_port = htons(13); // 端口号(主机字节序到网络字节序)
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        error_exit("bind"); 

    // 将上述套接字转换成监听套接字(监听描述符),这样客户的连接就可在该套接字上由内核接受
    if (listen(listenfd, 1024) < 0) // 指定系统内核允许在该监听描述符上排队的最大客户连接数
        error_exit("listen");

    for ( ; ; ) {
        // 睡眠等待客户的连接到达并被内核接受(三路握手)。握手完毕返回已连接描述符
        clilen = sizeof(cliaddr);
        if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) 
            error_exit("accept");                   
        printf("connection from %s, port %d\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), 
            ntohs(cliaddr.sin_port));   

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        // 发送应答(将当前时间字符串写给客户)
        if (write(connfd, buff, strlen(buff)) != strlen(buff))
            error_exit("write");

        // 终止连接(引发正常的终止序列:每个方向上发送一个FIN,每个FIN又由各自的对端确认)
        close(connfd); 
    }
    return 0;
}

daytimetcpcli.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define MAXLINE 32
#define error_exit(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr; // 网际套接字地址结构

    if (argc != 2) 
        error_exit("usage: a.out <IPaddress>");

    // 创建网际(AF_INET)字节流(SOCK_STREAM)套接字(TCP)
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
        error_exit("socket");

    // 建立连接:connect参数2强制转换成指向"通用套接字地址结构"的指针(开发此函数时,void*还不可用)   
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET; // 地址族
    servaddr.sin_port = htons(13); // 端口号(主机字节序到网络字节序)
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) // IP地址(呈现形式到数值)
        error_exit("inet_pton");
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        error_exit("connect");

    /* read函数读取服务器的应答,并用fputs输出结果“Thu Jan 12 10:45:52 2017\r\n”(26个字符)。
     * 因为TCP是一个没有记录边界的字节流协议,这26个字节可以有多种返回方式(包含26个字节的单个TCP分节、每个分节只含1个字节的26个分节等)。
     * 如果数据量很大,就不能确保一次read调用能返回服务器的整个应答,因此总把read编写在某个循环中。 */
    while ((n = read(sockfd, recvline, MAXLINE)) > 0) { // read()返回0:表明对端关闭连接
        recvline[n] = 0;
        if (fputs(recvline, stdout) == EOF) 
            error_exit("fputs");
    }
    if (n < 0) // read()返回负值:表明发生错误
        error_exit("read");

    exit(0); // Unix在一个进程终止时总是关闭该进程所有打开的描述符(TCP套接字就此被关闭)
}

结果:

$ ./daytimetcpcli 123.3.4.5
connect: Connection timed out (约75s后返回)

使用SIGALRM为connect设置超时

因为在多线程程序中正确使用信号非常困难,建议只是在未线程化或单线程化的程序中使用此技术。
(改写daytimetcpcli.c中connect调用如下:)

#include <signal.h>

void* Signal(int signo, void (*func)(int)) {
    struct sigaction act, oact;

    act.sa_handler = func; 
    sigemptyset(&act.sa_mask); 
    act.sa_flags = 0;
#ifdef SA_INTERRUPT 
    if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TCP/IP网络互连技术.卷Ⅲ.客户/服务器编程和应用(Windows套接字版)》是一本介绍TCP/IP网络编程以及客户/服务器应用的技术指南,主要针对使用Windows套接字进行网络编程的开发人员。该书总结了TCP/IP的基本原理、协议和常见应用,提供了丰富的示例代码和实用的案例,帮助读者快速掌握网络编程和客户/服务器应用的开发。 该书首先介绍了TCP/IP网络协议栈的结构和工作原理,讲解了套接字编程的基本概念和使用方法。然后,作者详细讲解了Windows平台上的套接字编程API,包括套接字的创建、绑定、监听、连接、发送和接收等操作。读者可以通过学习这些API,了解如何在Windows环境下进行网络编程。 接下来,该书介绍了常见的客户/服务器模型,并通过实例演示了如何开发客户/服务器应用。读者可以学习如何在服务器端创建套接字并监听客户端的连接请求,同时也可以学习如何在客户端创建套接字并与服务器建立连接。通过这些实例,读者可以了解服务器端和客户端之间的通信过程,以及如何处理多个客户端的并发连接。 此外,该书还介绍了如何实现简单的网络协议,如HTTP、FTP和SMTP等。通过详细的实例代码,读者可以学习到如何解析和生成这些协议的数据包,以及如何进行相应的网络通信。 总的来说,《TCP/IP网络互连技术.卷Ⅲ.客户/服务器编程和应用(Windows套接字版)》是一本适合想要学习TCP/IP网络编程和客户/服务器应用开发的读者的技术指南。通过学习这本书,读者可以深入了解网络编程的基本原理和技术,并能够使用Windows套接字进行网络应用的开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值