【Linux C编程】第十七章 socket编程

举报
Yuchuan 发表于 2021/05/14 20:30:12 2021/05/14
【摘要】 socktet 编程知识解析

一、套接字概念

    Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。

    既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要

应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

    套接字的内核实现较为复杂,不宜在学习初期深入学习。

    在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的

socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

    套接字通信原理如下图所示:

    在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符发送缓冲区和接收缓冲区。

    TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。本章的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX Domain

Socket的函数接口。

 二、预备知识

 1. 网络字节序

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

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

网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

    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位短整数。

    如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

  • 主机字节顺序  -->  网络字节顺序:
uint16_t htons(uint16_t hostshort);    端口
uint32_t htonl(uint32_t hostlong);    IP
  • 网络字节顺序 --> 主机字节顺序

uint16_t ntohs(uint16_t netshort);    端口
uint32_t ntohl(uint32_t netlong);    IP

 2. IP地址转换函数

    早期:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
只能处理IPv4的ip地址
注意参数是struct in_addr

    现在:

#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);

    其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。

    因此函数接口是void *addrptr。

3. sockaddr数据结构

    strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是

sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

struct sockaddr {
    sa_family_t sa_family;      /* address family, AF_xxx */
    char sa_data[14];           /* 14 bytes of protocol address */
};

    使用 sudo grep -r "struct sockaddr_in {"  /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。

struct sockaddr_in {
    __kernel_sa_family_t sin_family;             /* Address family */      地址结构类型
    __be16 sin_port;                             /* Port number */        端口号
    struct in_addr sin_addr;                    /* Internet address */    IP地址
    /* Pad to size of `struct sockaddr'. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};

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

struct sockaddr_in6 {
    unsigned short int sin6_family;         /* AF_INET6 */
    __be16 sin6_port;                     /* Transport layer port # */
    __be32 sin6_flowinfo;                 /* IPv6 flow information */
    struct in6_addr sin6_addr;            /* IPv6 address */
    __u32 sin6_scope_id;                 /* scope id (new in RFC2553) */
};
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;     /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX];     /* pathname */
};

    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 *类型表示,在传递参

数之前要强制类型转换一下,例如:

struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));        /* initialize servaddr */

三、套接字函数

1. socket模型创建流程图

socket API

2. socket函数

#include <sys/types.h> /* See NOTES */
#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 表示使用默认协议。
返回值:
    成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno

    socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,

domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。

protocol参数的介绍从略,指定为0即可。

 3. bind函数

#include <sys/types.h> /* See NOTES */
#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和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可

以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:

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

    首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以

在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

4. listen函数

#include <sys/types.h> /* See NOTES */
#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。

5. accept函数

#include <sys/types.h>         /* See NOTES */
#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-result argument),传入的是调用者提供的缓冲区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。

6. connect函数

#include <sys/types.h>                     /* See NOTES */
#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()成功返回0,出错返回-1。

7.  数据接收

ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

8. 数据发送

ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

flags 赋值为0

四、C/S模型-TCP

    下图是基于TCP协议的客户端/服务器程序的一般流程:

    服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一

个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

    数据传输的过程:

    建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用

read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞

等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用

close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 应用程序如何知道TCP协议层的状态变

化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

1. server

    下面通过最简单的客户端/服务器程序的实例来学习socket API。

    server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

server.c

1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <stdlib.h>
 4 #include <sys/types.h>
 5 #include <sys/stat.h>
 6 #include <string.h>
 7 #include <sys/socket.h>
 8 #include <arpa/inet.h>
 9 #include <ctype.h>
10 
11 
12 int main(int argc, const char* argv[])
13 {
14    
15     int lfd = socket(AF_INET, SOCK_STREAM, 0);
16     if(lfd == -1)
17     {
18         perror("socket error");
19         exit(1);
20     }
21 
22 
23     struct sockaddr_in serv_addr;
24    
25     memset(&serv_addr, 0, sizeof(serv_addr));
26  
27     serv_addr.sin_family = AF_INET; 
28     serv_addr.sin_port = htons(6666);  
29     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
30 
31     int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
32     if(ret == -1)
33     {
34         perror("bind error");
35         exit(1);
36     }
37 
38 
39     ret = listen(lfd, 64);
40     if(ret == -1)
41     {
42         perror("listen error");
43         exit(1);
44     }
45 
46   
47     struct sockaddr_in cline_addr;
48     socklen_t clien_len = sizeof(cline_addr);
49     int cfd = accept(lfd, (struct sockaddr*)&cline_addr, &clien_len);
50     if(cfd == -1)
51     {
52         perror("accept error");
53         exit(1);
54     }
55     
56     char ipbuf[64];
57    
58     printf("cliient ip: %s, port: %d\n",
59            inet_ntop(AF_INET, &cline_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
60            ntohs(cline_addr.sin_port));
61 
62 
63     while(1)
64     {
65      
66         char buf[1024] = {0};
67         int len = read(cfd, buf, sizeof(buf));
68         if(len == -1)
69         {
70             perror("read error");
71             break;
72         }
73         else if(len > 0)
74         {
75          
76             printf("read buf = %s\n", buf);
77             // 小写 -》 大写
78             for(int i=0; i<len; ++i)
79             {
80                 buf[i] = toupper(buf[i]);
81             }
82             printf(" -- toupper: %s\n", buf);
83 
84             // 数据发送给客户端
85             write(cfd, buf, strlen(buf)+1);
86         }
87         else if( len == 0 )
88         {
89             printf("client disconnect ...\n");
90             break;
91         }
92     }
93 
94     close(lfd);
95     close(cfd);
96 
97     return 0;
98 }

2. client

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

client.c

1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <stdlib.h>
 4 #include <sys/types.h>
 5 #include <sys/stat.h>
 6 #include <string.h>
 7 #include <arpa/inet.h>
 8 #include <fcntl.h>
 9 
10 // tcp client
11 int main(int argc, const char* argv[])
12 {
13     // 创建套接字
14     int fd = socket(AF_INET, SOCK_STREAM, 0);
15     if(fd == -1)
16     {
17         perror("socket error");
18         exit(1);
19     }
20 
21     // 连接服务器
22     struct sockaddr_in serv_addr;
23     memset(&serv_addr, 0, sizeof(serv_addr));
24     serv_addr.sin_family = AF_INET;
25     serv_addr.sin_port = htons(9999);
26     inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
27     int ret = connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
28     if(ret == -1)
29     {
30         perror("connect error");
31         exit(1);
32     }
33 
34     // 通信
35     while(1)
36     {
37         // 写数据
38         // 接收键盘输入
39         char buf[512];
40         fgets(buf, sizeof(buf), stdin);
41         // 发送给服务器
42         write(fd, buf, strlen(buf)+1);
43 
44         // 接收服务器端的数据
45         int len = read(fd, buf, sizeof(buf));
46         if (len == -1)
47         {
48             perror("read error");
49             exit(1);
50         }
51         else if (len == 0)
52         {
53             printf("server closed\n");
54             break;
55         }
56         else
57         {
58             printf("read buf = %s, len = %d\n", buf, len);
59         }
60     }
61     
62     close(fd);
63     
64     return 0;
65 }

    由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用

bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

    客户端和服务器启动后可以使用netstat命令查看链接情况:

netstat -apn|grep 6666

五、出错处理封装函数

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

息。

    为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c及wrap.h:

wrap.c

1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #include <unistd.h>
  4 #include <errno.h>
  5 #include <sys/socket.h>
  6 
  7 void perr_exit(const char *s)
  8 {
  9     perror(s);
 10     exit(-1);
 11 }
 12 
 13 int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
 14 {
 15     int n;
 16 
 17 again:
 18     if ((n = accept(fd, sa, salenptr)) < 0) 
 19     {
 20         //ECONNABORTED 发生在重传(一定次数)失败后,强制关闭套接字
 21         //EINTR 进程被信号中断
 22         if ((errno == ECONNABORTED) || (errno == EINTR))
 23         {
 24             goto again;
 25         }
 26         else
 27         {
 28             perr_exit("accept error");
 29         }
 30     }
 31     return n;
 32 }
 33 
 34 int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
 35 {
 36     int n;
 37 
 38     if ((n = bind(fd, sa, salen)) < 0)
 39     {
 40         perr_exit("bind error");
 41     }
 42 
 43     return n;
 44 }
 45 
 46 int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
 47 {
 48     int n;
 49     n = connect(fd, sa, salen);
 50     if (n < 0) 
 51     {
 52         perr_exit("connect error");
 53     }
 54 
 55     return n;
 56 }
 57 
 58 int Listen(int fd, int backlog)
 59 {
 60     int n;
 61 
 62     if ((n = listen(fd, backlog)) < 0)
 63     {
 64         perr_exit("listen error");
 65     }
 66 
 67     return n;
 68 }
 69 
 70 int Socket(int family, int type, int protocol)
 71 {
 72     int n;
 73 
 74     if ((n = socket(family, type, protocol)) < 0)
 75     {
 76         perr_exit("socket error");
 77     }
 78 
 79     return n;
 80 }
 81 
 82 ssize_t Read(int fd, void *ptr, size_t nbytes)
 83 {
 84     ssize_t n;
 85 
 86 again:
 87     if ( (n = read(fd, ptr, nbytes)) == -1) 
 88     {
 89         if (errno == EINTR)
 90             goto again;
 91         else
 92             return -1;
 93     }
 94 
 95     return n;
 96 }
 97 
 98 ssize_t Write(int fd, const void *ptr, size_t nbytes)
 99 {
100     ssize_t n;
101 
102 again:
103     if ((n = write(fd, ptr, nbytes)) == -1) 
104     {
105         if (errno == EINTR)
106             goto again;
107         else
108             return -1;
109     }
110     return n;
111 }
112 
113 int Close(int fd)
114 {
115     int n;
116     if ((n = close(fd)) == -1)
117         perr_exit("close error");
118 
119     return n;
120 }
121 
122 /*参三: 应该读取的字节数*/                          
123 //socket 4096  readn(cfd, buf, 4096)   nleft = 4096-1500
124 ssize_t Readn(int fd, void *vptr, size_t n)
125 {
126     size_t  nleft;              //usigned int 剩余未读取的字节数
127     ssize_t nread;              //int 实际读到的字节数
128     char   *ptr;
129 
130     ptr = vptr;
131     nleft = n;                  //n 未读取字节数
132 
133     while (nleft > 0) 
134     {
135         if ((nread = read(fd, ptr, nleft)) < 0) 
136         {
137             if (errno == EINTR)
138             {
139                 nread = 0;
140             }
141             else
142             {
143                 return -1;
144             }
145         } 
146         else if (nread == 0)
147         {
148             break;
149         }
150 
151         nleft -= nread;   //nleft = nleft - nread 
152         ptr += nread;
153     }
154     return n - nleft;
155 }
156 
157 ssize_t Writen(int fd, const void *vptr, size_t n)
158 {
159     size_t nleft;
160     ssize_t nwritten;
161     const char *ptr;
162 
163     ptr = vptr;
164     nleft = n;
165     while (nleft > 0) 
166     {
167         if ( (nwritten = write(fd, ptr, nleft)) <= 0) 
168         {
169             if (nwritten < 0 && errno == EINTR)
170                 nwritten = 0;
171             else
172                 return -1;
173         }
174         nleft -= nwritten;
175         ptr += nwritten;
176     }
177     return n;
178 }
179 
180 static ssize_t my_read(int fd, char *ptr)
181 {
182     static int read_cnt;
183     static char *read_ptr;
184     static char read_buf[100];
185 
186     if (read_cnt <= 0) {
187 again:
188         if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)    //"hello\n"
189         {
190             if (errno == EINTR)
191                 goto again;
192             return -1;
193         } 
194         else if (read_cnt == 0)
195             return 0;
196 
197         read_ptr = read_buf;
198     }
199     read_cnt--;
200     *ptr = *read_ptr++;
201 
202     return 1;
203 }
204 
205 /*readline --- fgets*/    
206 //传出参数 vptr
207 ssize_t Readline(int fd, void *vptr, size_t maxlen)
208 {
209     ssize_t n, rc;
210     char    c, *ptr;
211     ptr = vptr;
212 
213     for (n = 1; n < maxlen; n++) 
214     {
215         if ((rc = my_read(fd, &c)) == 1)    //ptr[] = hello\n
216         {
217             *ptr++ = c;
218             if (c == '\n')
219                 break;
220         } 
221         else if (rc == 0) 
222         {
223             *ptr = 0;
224             return n-1;
225         } 
226         else
227             return -1;
228     }
229     *ptr = 0;
230 
231     return n;
232 }

wrap.h

1 #ifndef __WRAP_H_
 2 #define __WRAP_H_
 3 
 4 void perr_exit(const char *s);
 5 int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
 6 int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
 7 int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
 8 int Listen(int fd, int backlog);
 9 int Socket(int family, int type, int protocol);
10 ssize_t Read(int fd, void *ptr, size_t nbytes);
11 ssize_t Write(int fd, const void *ptr, size_t nbytes);
12 int Close(int fd);
13 ssize_t Readn(int fd, void *vptr, size_t n);
14 ssize_t Writen(int fd, const void *vptr, size_t n);
15 ssize_t my_read(int fd, char *ptr);
16 ssize_t Readline(int fd, void *vptr, size_t maxlen);
17 
18 #endif

   使用上面的封装实现一个server和client:

server.c

1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/types.h>
 4 #include <sys/socket.h>
 5 #include <strings.h>
 6 #include <string.h>
 7 #include <ctype.h>
 8 #include <arpa/inet.h>
 9 
10 #include "wrap.h"
11 
12 #define SERV_PORT 6666
13 
14 int main(void)
15 {
16     int sfd, cfd;
17     int len, i;
18     char buf[BUFSIZ], clie_IP[128];
19 
20     struct sockaddr_in serv_addr, clie_addr;
21     socklen_t clie_addr_len;
22 
23     sfd = Socket(AF_INET, SOCK_STREAM, 0);
24 
25     bzero(&serv_addr, sizeof(serv_addr));           
26     serv_addr.sin_family = AF_INET;                 
27     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
28     serv_addr.sin_port = htons(SERV_PORT);          
29 
30     Bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
31 
32     Listen(sfd, 2);                                
33 
34     printf("wait for client connect ...\n");
35 
36     clie_addr_len = sizeof(clie_addr_len);
37     cfd = Accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
38     printf("cfd = ----%d\n", cfd);
39 
40     printf("client IP: %s  port:%d\n", 
41             inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
42             ntohs(clie_addr.sin_port));
43 
44     while (1) 
45     {
46         len = Read(cfd, buf, sizeof(buf));
47         Write(STDOUT_FILENO, buf, len);
48 
49         for (i = 0; i < len; i++)
50             buf[i] = toupper(buf[i]);
51         Write(cfd, buf, len); 
52     }
53 
54     Close(sfd);
55     Close(cfd);
56 
57     return 0;
58 }

client.c

1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <string.h>
 4 #include <sys/socket.h>
 5 #include <arpa/inet.h>
 6 
 7 #include "wrap.h"
 8 
 9 #define SERV_IP "127.0.0.1"
10 #define SERV_PORT 6666
11 
12 int main(void)
13 {
14     int sfd, len;
15     struct sockaddr_in serv_addr;
16     char buf[BUFSIZ]; 
17 
18     sfd = Socket(AF_INET, SOCK_STREAM, 0);
19 
20     bzero(&serv_addr, sizeof(serv_addr));                       
21     serv_addr.sin_family = AF_INET;                             
22     inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);    
23     serv_addr.sin_port = htons(SERV_PORT);                      
24 
25     Connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
26 
27     while (1) {
28         fgets(buf, sizeof(buf), stdin);
29         int r = Write(sfd, buf, strlen(buf));       
30         printf("Write r ======== %d\n", r);
31         len = Read(sfd, buf, sizeof(buf));
32         printf("Read len ========= %d\n", len);
33         Write(STDOUT_FILENO, buf, len);
34     }
35 
36     Close(sfd);
37 
38     return 0;
39 }

makefile

1 src = $(wildcard *.c)
 2 obj = $(patsubst %.c, %.o, $(src))
 3 
 4 all: server client
 5 
 6 server: server.o wrap.o
 7     gcc server.o wrap.o -o server -Wall
 8 client: client.o wrap.o
 9     gcc client.o wrap.o -o client -Wall
10 
11 %.o:%.c
12     gcc -c $< -Wall
13 
14 .PHONY: clean all
15 clean: 
16     -rm -rf server client $(obj)


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。