Linux系统编程之Socket编程

本章节主要介绍Linux下如何通过系统提供的API接口实现Socket编程。通过结合之前几篇博文中的内容,包括网络基础,TCP/IP模型以及其之间建议通信的链接细节,从理论到代码完整的贯通,毕竟Socket编程的重要性,对于程序员来说不言而喻了。

Socket编程

开胃菜

在TCP/IP协议中,“IP地址+TCP或UDP端口号”就唯一标识网络通讯中的一个进程,因此,“IP 地址+端口号”就称为socket。

在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。

TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。

再介绍具体的编程之前,我们还需要了解一下几个重要概念。

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h表示host,n表示network,l表示32位长整数,s表示16位短整数
如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,这些函数不作转换,将参数原封不动的返回。

IP地址相关函数

IP转换函数再早期只支持IPv4,也不支持可重入,具体如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);

在后续编程中,使用下面新的API接口实现方式:

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

支持IPv4和IPv6
可重入函数

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

这里就不先贴代码了,总体先有个印象,后续代码中会有详细注释。

结构体sockaddr

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

struct sockaddr {
sa_family_t sa_family;
char  sa_data[14];
};

struct sockaddr_in {
__kernel_sa_family_t  sin_family;
__be16 sin_port;
struct in_addr sin_addr;

unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};

/* Internet address. */
struct in_addr {
__be32  s_addr;
};

struct sockaddr_in6 {
unsigned short int  sin6_family;
__be16 sin6_port;
__be32 sin6_flowinfo;
struct in6_addr sin6_addr;
__u32 sin6_scope_id;
};

struct in6_addr {
union {
    __u8
    u6_addr8[16];
    __be16 u6_addr16[8];
    __be32 u6_addr32[4];
} in6_u;

#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};

#define UNIX_PATH_MAX 108

struct sockaddr_un {
__kernel_sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};

IPv4和IPv6的地址格式定义在 netinet/in.h 中,IPv4地址用 sockaddr_in 结构体表示,包括16位端口号和32位IP地址,IPv6地址用 sockaddr_in6 结构体表示,包括16位端口号、128位IP地址和一些控制字段。

UNIX Domain Socket的地址格式定义在 sys/un.h 中,用 sock_addr_un 结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。

IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。

因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void 类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void 类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:

bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

更多关于sockaddr的信息参考以下链接

关于sockaddr的相关解释

食材

在介绍具体的编程模型之前,我们先来了解一下其中涉及到的重要函数,只有了解了这些函数的相关概念,在后续的实战编程中,我们结合模型,才能更好的理解其流程。

socket

创建一个socket网络通讯端口。

函数原型

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain:
    AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
    AF_INET6 与上面类似,不过是来用IPv6的地址
    AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
    SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
    SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
    SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
    SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
    SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
    0 默认协议
返回值:
    成功返回一个新的文件描述符,失败返回-1,设置errno

socket()打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描用出错则返回 -1。对于 IPv4,domain 参数指定为 AF_INET。对于TCP协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM,表示面向数据报的传输协议。protocol 参数的介绍从略,指定为 0 即可。

bind

执行socket端口与设备端口号和ip地址绑定。

函数原型

  #include <sys/types.h>
  #include <sys/socket.h>

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

  sockfd:
      socket文件描述符
  addr:
      构造出IP地址加端口号
  addrlen:
      sizeof(addr)长度
  返回值:
成功返回0,失败返回-1, 设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址和端口号。

bind()的作用是将参数 sockfd 和 结构体 sockaddr addr绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。struct sockaddr *是一个通用指针类型,addr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度。函数接口会根据其长度自动分析出其地址是 IPV4 还是 IPV6,例如:

struct sockaddr_in servaddr;
// 清零
bzero(&servaddr, sizeof(servaddr));
// 指定 IP地址为 IPV4
servaddr.sin_family = AF_INET;
// 任意本地 IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);

首先将整个结构体清零,然后设置地址类型为 AF_INET,网络地址为 INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为8000。

listen

接受客户端连接,就绪等待处理。

函数原型

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:
    socket文件描述符
backlog:
    排队建立3次握手队列和刚刚建立3次握手队列的链接数和

查看系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态, listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen() 成功返回0,失败返回 -1。

accept

接受客户端的连接请求,得到一个用于读写数据的通道标识符。

函数原型

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
    socket文件描述符
addr:
    传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
    传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
    成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr 是一个传出参数,accept() 返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数(value-resultargument),传入的是调用者提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 addr 参数传 NULL,表示不关心客户端的地址。

我们的服务器程序结构是这样的:

while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ......
    close(connfd);
}

整个是一个 while 死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept() 之前应该重新赋初值。 accept() 的参数 listenfd 是先前的监听文件描述符,而 accept() 的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd 断开连接,而不关闭 listenfd,再次回到循环开头 listenfd 仍然用作 accept 的参数。accept() 成功返回一个文件描述符,出错返回 -1。

connect

通过 accept 返回的新文件描述符,建立数据交互的连接通道。

函数原型

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
    socket文件描述符
addr:
    传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
    传入参数,传入sizeof(addr)大小
返回值:
    成功返回 0,失败返回 -1,设置 errno

客户端需要调用 connect() 连接服务器,connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。服务器可以通过 connect 中的具体参数获取到当前已连接的客户端的 ip 地址和端口号。connect() 成功返回 0,出错返回 -1。

美食1-TCP

上面具体介绍了Socket编程中用的几个重要函数,下面就来实战Socket编程中TCP的代码实现。

TCP协议的通信流程在之前TCP、UDP章节中具体分析过了,包括其三次握手建立连接,四次握手断开链接,连接中状态装换以及滑动窗口等概念和原理。下图具体展示了TCP协议的通信模型,包括客户端和服务器连接过程中具体函数接口的调用时机,两端设备端口的状态,数据导向等,再此基础上加深理解具体代码的实现过程。

Server

服务器的工作任务很简单,根据连接上的客户端传来的数据,进行转大写回传。

#include "wrap.h"
#include <stdlib.h>
#include <stdio.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <ctype.h>
#include <string.h>
#include <unistd.h>

// 服务端端口定义 8000
#define SERVER_PORT 8000
// 缓冲区 4096
#define BUF_SIZE 4096

int main(int argc, char *argv[]){

    int sockfd, confd, readlen;
    struct sockaddr_in serveraddr,clientaddr;
    socklen_t clientaddr_len;
    char clientIP[128];
    char buf[BUF_SIZE];
    // 新建Socket通道,指定TCP协议
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);
    // 填充结构体 sockaddr_in
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVER_PORT);
    // 绑定指定socket
    Bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    // 开始监听
    Listen(sockfd,30);
    printf(" Accepting connections ...\n");

    // 死循环等待客户链接
    while(1){
        clientaddr_len = sizeof(clientaddr);
        // 连接上客户端
        confd = Accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
        // 打印已链接的客户端数据
        printf("client IP: %s, Port: %d \n",
            inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr,clientIP, sizeof(clientIP)),
            ntohs(clientaddr.sin_port)
            );
        // 获取客户端传来数据
        readlen = Read(confd,buf,sizeof(buf));

        int i = 0;
        while(i<readlen){
            // 小写转大写
            buf[i] = toupper(buf[i]);
            i++;
        }
        // 回传客户端
        Write(confd,buf,readlen);
        Close(confd);
    }
}

Client

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "wrap.h"

// 与服务端的一致
#define SERVER_PORT 8000
#define BUF_SIZE 4096

int main(int argc, char * argv[]){

    int serverfd, readlen;
    struct sockaddr_in serveraddr;
    // ServerIP,可以自己指定自己的IP,也可以默认本地IP
    char serverIP[] = "127.0.0.1";
    char buf[BUF_SIZE];

    if(argc < 2){
        printf("./client agc...\n");
        exit(1);
    }

    serverfd = Socket(AF_INET, SOCK_STREAM, 0);
    // 清空结构体serveraddr
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    // IP地址转换
    inet_pton(AF_INET, serverIP, &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(SERVER_PORT);
   // 连接服务器
    Connect(serverfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    Write(serverfd, argv[1],sizeof(argv[1]));
    readlen = Read(serverfd,buf,sizeof(buf));
    Write(STDOUT_FILENO, buf,readlen);

    Close(serverfd);
}

由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用 bind(),只是没有必要调用 bind() 固定一个端口号,服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

客户端和服务器启动后可以查看链接情况:

netstat -apn|grep 8000

调试相关说明:

  1. 启动shell窗口,运行服务 ./server
  2. 新建shell窗口,再运行Client客户端,记得添加需要转换的额外数据,例如: ./client abcdefg
  3. 连接成功后,服务器窗口返回已连接客户端的ip地址和端口号,客户端则会打印命令中额外参数对应的大写数据,对应上面的则是:ABCDEFG

美食2-UDP

这里我们来看一下 UDP 的实战编程。由于 UDP 不需要维护连接,程序逻辑简单了很多,但是 UDP 协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现。

UDP 通信模型如下图:

UDP 的通信模型和 TCP 在建立链接之前的配置基本相似,但是对于后面建立连接后的数据通信过程就简单多了。sendto 负责发送数据,recvfrom 负责接受数据,其每次数据传输过程中(不论是发送还是接受数据),都需要传入地址参数,而这一切在 TCP 模型中,都被 connect 取代了。

Server

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

#define SERVER_PORT 8001
#define BUF_SIZE 1024

int main(int argc, char const *argv[])
{
    int sockfd, readlen;

    char buf[BUF_SIZE];
    char clientIP[INET_ADDRSTRLEN];
    socklen_t clientlen;
    struct sockaddr_in serveraddr, clientaddr;
// 创建网络通信端口,指定SOCK_DGRAM为UDP
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 设置服务器端口信息
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family =  AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVER_PORT);
// 执行绑定
    bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    printf("Accepting connections ...\n");

    while(1){
        bzero(&clientaddr,sizeof(clientaddr));
        clientlen = sizeof(clientlen);
    // 从已连接的客户端读数据,传出参数clientaddr包含客户端信息
        readlen = recvfrom(sockfd, buf, BUF_SIZE, 0,
            (struct sockaddr *)&clientaddr,&clientlen);

        printf("client IP: %s, Port: %d \n",
            inet_ntop(AF_INET, &clientaddr.sin_addr,clientIP, sizeof(clientIP)),
            ntohs(clientaddr.sin_port)
            );

        int i = 0;
        while(i<readlen){
            buf[i] = toupper(buf[i]);
            i++;
        }
    // 回传数据到客户端,传出参数客户端clientaddr结构体信息
        sendto(sockfd,buf,readlen,0,
            (struct sockaddr *)&clientaddr,sizeof(clientaddr));

    }
    close(sockfd);
    return 0;
}

Client

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

#define SERVER_PORT 8001
#define BUF_SIZE 1024

int main(int argc, char * argv[]){

    int serverfd, readlen;
    struct sockaddr_in serveraddr;
    char serverIP[] = "127.0.0.1";
    char buf[BUF_SIZE];
    socklen_t serverlen;

    if(argc < 2){
        printf("./client agc...\n");
        exit(1);
    }

    serverfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, serverIP, &serveraddr.sin_addr);
    serveraddr.sin_port = htons(SERVER_PORT);

    sendto(serverfd,argv[1],sizeof(argv[1]),0,
        (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    bzero(&serverlen,sizeof(serverlen));
    readlen = recvfrom(serverfd,buf,BUF_SIZE,0,
        NULL, 0);

    write(STDOUT_FILENO, buf,readlen);

    close(serverfd);
    return 0;
}

UDP 模型下的实例代码,运行流程和 TCP 相似,首先启动 Server,然后开启 Client,发送数据到服务器进行大写转换,客户端读取收到的数据打印出来。

相信有读者已经发现了其两者之间通信模型的差距,其实理解起来也很容易,TCP 相当于打电话,开始接通之前喂喂喂确认是对方后,知道挂断前都不用再确认对方身份,也就是在数据交互过程中不需要夹杂地址信息了;然而 UDP 则必须每次数据交互中都需要填入对方地址信息,就犹如寄信,每次张信都需要地址和邮票一样。

总的话来说,还是其通信的本质有区别,TCP 是基于数据流方式,UDP 是基于消息方式。

饭后甜点

上面的两个实例不仅功能简单,而且简单到几乎没有什么错误处理。然而在系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。

为使错误处理的代码不影响主程序的可读性,我们把与 socket 相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块 wrap.c,提供一个头文件 wrap.h 依赖,具体的wrap.c 如下:

头文件

#ifndef WRAP_H
#define WRAP_H

#include <stdlib.h>
#include <sys/socket.h>
#include <errno.h>

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif // WRAP_H

### 实现文件

#include "wrap.h"

void perr_exit(const char *s)
{
    perror(s);
    exit(1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
    int n;
again:
    if((n = accept(fd,sa,salenptr)) < 0){
        if((errno == ECONNABORTED) || (errno == EINTR)){
            goto again;
        }else {
            perr_exit("accept");
        }
        return n;
    }
}

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

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

void Listen(int fd, int backlog)
{
    if (listen(fd, backlog) < 0)
        perr_exit("listen error");
}

int Socket(int family, int type, int protocol)
{
    int n;
    if((n = socket(family,type,protocol))<0)
        perr_exit("socket");

    return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
    ssize_t n;

again:
    if((n = read(fd,ptr,nbytes))==-1){
        if(errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
    ssize_t n;
again:
    if ( (n = write(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

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

ssize_t Readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char
            *ptr;
    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
    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;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

ssize_t my_read(int fd, char *ptr)
{
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];
    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;
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;
        } else
            return -1;
    }
    *ptr = 0;
    return n;
}

对于上述模块,可以采用以下几种调用方式

  1. 源代码直接包含,简单直接,在 Makefile 中直接声明 -I 方式将头文件包含进去,记得 wrap.c 文件需要和源文件 server.c client.c 同级目录。
  2. 直接包含头文件,源文件以动态库形式存在,隐藏源码,需要事先将模块编译成 so 动态链接库,具体方法,请参考之前静态、动态链接库实战章节。

真香

本章节介绍了Socket编程下,TCP,UDP模型的实例代码。由于其通信模型的不同,其代码实现上也有不同,但是大同小异。相同的是建立连接服务过程中,服务端必须指定ip地址和端口号,绑定后,客户端连接时也要和服务器的一致;不同的是在数据交互过程中,TCP基于流,一旦建立链接(connect)后就可以直接数据传输,无需地址信息的参与,而UDP则需要在数据交互中,有一个传入传出参数负责地址的接受和发送,每次发送数据都需要将接收数据的地址信息包含进去,谁发来,回传给谁。

总而言之,对于上述代码实现以及章节中的模型图,相信对这两种通信协议能有很好的理解。

邢文鹏Linux教学资料

坚持原创技术分享,您的支持将鼓励我继续创作!