APUE笔记---网络IPC:socket套接字使用+聊天程序

APUE笔记—网络IPC:socket套接字使用+聊天程序

1. 套接字描述符的创建与销毁

套接字是通信端点的抽象,套接字描述符是一种文件描述符。

1.1创建套接字描述符

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

int socket(int domain, int type, int protocol);
//返回值:若成功,返回文件(套接字)描述符,若出错,返回-1
  • 参数domain(域)确定通信特性,包括地址格式。下图列出部分:

|域|描述|
|:—:|:—:|
|AF_INET|IPv4因特网域|
|AF_INET6|IPv6因特网域|
|AF_UNIX,AF_LOCAL|UNIX域|

  • 参数type 确定套接字的类型,进一步确定通信特征。

|类型|描述|协议|
|:—:|:—:|:—:|
|SOCK_STREAM|有序、可靠、双向、面向连接的字节流|TCP|
|SOCK_DGRAM|固定长度、无连接、不可靠的报文传递|UDP|
|SOCK_SEQPACKET|固定长度、有序、可靠、面向连接的报文传递|SCTP|
|SOCK_RAW|原始套接字|自行构造协议头部|

  • 参数protocol通常为0,表示给定的域和套接字类型选择默认协议。

1.2销毁套接字

套接字通信是双向的,可以采用shutdown函数禁止一个套接字的IO

#include <sys/socket.h>

int shutdown(int sockfd, int how);
//返回值:若成功,返回0;若出错,返回-1.
  • 参数how有三种选项

|how|描述|
|:—:|:—:|
|SHUT_RD|关闭读端|
|SHUT_WR|关闭写端|
|SHUT_RDWR|关闭读写|

shutdown函数和close函数的区别

  • close函数用于关闭一个文件描述符,只是将文件描述符的引用计数减1,当引用计数减为0,则释放该文件描述符,否则引用该文件描述符的进程正常使用。
  • shutdown函数用于禁止一个套接字的IO,无论它的文件描述符是否为0,都会设定套接字处于设定状态,并且引用该文件描述符的进程受影响。

2. 寻址

网络进程标示由两部分组成,并且唯一确定

  1. 计算机的网络地址,也就是IP地址。
  2. 计算机的端口号。

2.1字节序

在不同的处理器的存放方式主要有两种,以内存中0x0A0B0C0D的存放方式为例,分别有以下几种方式:
大端序
数据以8bit为单位:

地址增长方向 →
| … | 0x0A | 0x0B | 0x0C | 0x0D| …|

示例中,最高位字节是0x0A 存储在最低的内存地址处。下一个字节0x0B存在后面的地址处。
小端序
数据以8bit为单位:

地址增长方向 →
| … | 0x0D | 0x0C | 0x0B | 0x0A | … |
最低位字节是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

TCP/IP协议栈使用大端序,所以在网络传输之前要进行字节序转换
有四个用来在处理器字节序和网络字节序之间实施转换的函数

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
//返回值:以网络字节序标示的32位整数
uint16_t htons(uint16_t hostshort);
//返回值:以网络字节序标示的16位整数
uint32_t ntohl(uint32_t netlong);
//返回值:以主机字节序标示的32位整数
uint16_t ntohs(uint16_t netshort);
//返回值:以主机字节序标示的16位整数
  • h表示“主机”字节序, n表示“网络”字节序。
  • l表示“长”整形,4字节,32位;s表示“短”整形,16位。

2.2地址格式

一个地址标识一个特定通信域的套接字端点,地址格与这个特定的通信相关,为使不同的地址格式传入套接字函数,地址会被强行转换成一个通用的地址结构sockaddr;

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

//在IPv4因特网域(AF_INET)中,套接字地址用结构sockaddr_in表示
struct sockaddr_in {
  __kernel_sa_family_t  sin_family;     /* Address family               */
  __be16                sin_port;       /* Port number                  */
  struct in_addr        sin_addr;       /* Internet address             */

  /* Pad to size of `struct sockaddr'. */
  unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};

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

//在IPv6因特网域(AF_INET6)中,套接字地址用结构sockaddr_in6表示
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];
#if __UAPI_DEF_IN6_ADDR_ALT
                __be16          u6_addr16[8];
                __be32          u6_addr32[4];
#endif
        } in6_u; 
//这个联合体大小是128字节,但是通过不同的分类,当读取ip地址时,会有不同的读取方式。
#define s6_addr                 in6_u.u6_addr8
#if __UAPI_DEF_IN6_ADDR_ALT
#define s6_addr16               in6_u.u6_addr16
#define s6_addr32               in6_u.u6_addr32
#endif
};

二进制地址格式与点分式十进制字符表示(a.b.c.d)之间相互转换,函数inet_ntop和inet_pton同时适用于IPv4和IPv6地址。

#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//返回值:若成功,返回地址字符串指针,若出错,返回NULL

int inet_pton(int af, const char *src, void *dst);
//返回值:若成功,返回1,若格式无效,返回0,若出错,返回-1
  • 参数af可以是AF_INET,也可以是AF_INET6,若以不被支持的地址族作为af参数,则两个函数返回错误,并且设置errno为EAFNOSUPPORT
  • inet_pton函数将文本字符串格式转换成网络字节序的二进制地址。尝试转换src指向的字符串,并通过dst指针保存二进制结果。
  • inet_ntop函数将网络字节序的二进制地址转换为文本字符串格式。从数值格式(stc)转换成表达式格式(dst)。size是目标存储单元的大小,以免缓冲区溢出。

2.3 将套接字与地址关联

使用bind函数关联地址和套接字。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//返回值:若成功,返回0;若出错,返回-1
  • bind将addr所指向的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出socket地址的长度。
  • 在进程正在运行的计算机上,指定的地址必须有效,不能指定一个其他机器的地址
  • 地址必须和创建套接字时的地址组所支持的格式相匹配。
  • 地址中国的端口号必须不小于1024,除非该进程有相应的特权。
  • 一般只能将一个套接字端点绑定到一个给定的地址上。
    对于因特网域,如果IP地址为INADDR_ANY,套接字端点可以被绑定到所有系统网络接口上。这意味可以接收这个系统所安装的任何一个网卡的数据包。

3. 建立连接

3.1 使用connect函数来建立连接

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//返回值:若成功,返回0;若出错,返回-1

在connect中指定的地址是要通信的目标地址。如果sockfd没有绑定地址,connect会给调用者一个默认的地址。

3.2 服务器调用listen函数来接收连接请求

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
//返回值:若成功,返回0;若出错,返回-1
  • 参数backlog提供一个提示,提示系统该进程要入队的未完成连接请求的数量。默认值为128,由

3.3 accept函数获得连接请求并建立连接

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//返回值:若成功,返回文件(套接字)描述符,若出错,返回-1
  • 函数accept返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。该套接字描述符和原始套接字具有相同的套接字类型和地址族。
  • 若不关心客户端标识,可以将参数addr和len设为NULL。否则在调用accept之前,将addr参数设为足够大的缓冲区来存放地址,并且将len指向的整数设为这个缓冲区的字节大小。返回时,accept会在缓冲区填充客户端的地址,并且更新指向len的整数来反应该地址的大小。
  • 如果没有连接请求,accept会处于阻塞状态等待请求到来

4. 数据传输

对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,增加了对数据读写的控制,用于TCP流数据读写的系统调用:

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

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//返回值:返回数据的字节长度,若无可用数据或对等方已经按序结束,返回0,如出错,返回-1。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//返回值:若成功,返回发送的字节数,如出错,返回-1

5. 带外数据

  • 带外数据(out-of-band-data)是一些通信协议所支持的可选功能,与普通数据相比,它允许更高优先级的数据传输,带外数据先行传输即使传输队列已经有数据。TCP支持带外数据,但UDP不支持。
  • TCP将带外数据称为紧急数据(urgent data)。TCP仅仅支持一个字节的紧急数据,,但是允许紧急数据在普通数据传递机制数据流之外传输。
  • 用过send函数的的flags参数指定一个MSG_OOB标志可以产生紧急数据,如果带外数据超过一个字节,则视最后一个字节为紧急数据。

6.客户端和服务器端相互发送数据的简单实现

6.1服务器端程序代码

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

int main(int argc, char *argv[])
{
        struct sockaddr_in clientaddr, serveraddr;
        int sockfd;
        int connfd;
        int addrlen;

        //1.创建socket文件描述符用于处理监听
        sockfd = socket(AF_INET, SOCK_STREAM, 0);

        //2.初始化服务器端的地址族,端口,和IP地址
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);

        //3.将sockfd文件描述符和服务器地址绑定
        bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        //4.监听,系统默认队列为128
        listen(sockfd, 128);

        while(1){
                //5.接受客户端连接,返回一个connfd文件描述符用与处理连接
                addrlen = sizeof(clientaddr);
                connfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);

                int ret;
                do{
                        //6.相应请求,先接受客户发送信息并答应,然后发送信息
                        char buf[1024];
                        memset(buf, '\0', sizeof(buf));
                        ret = recv(connfd, buf, sizeof(buf), 0);
                        write(STDOUT_FILENO, buf, strlen(buf));
                        char dst[128];
                        printf("from ip:%s\tport:%d\n", inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, dst, sizeof(dst)), ntohs(clientaddr.sin_port));
                        printf("*-------------------------------------*\n");
                        memset(buf, '\0', sizeof(buf));
                        read(STDOUT_FILENO, buf, sizeof(buf));
                        send(connfd, buf, strlen(buf), 0);
                        printf("msg is sent\n");
                        printf("*-------------------------------------*\n");

                }while(ret);

                //7.关闭该用于连接的文件描述符
                close(connfd);
        }
    //8.关闭用于监听的文件描述符
        close(sockfd);

        return 0;
}

6.2客户端程序代码

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

int main(int argc, char *argv[])
{
        struct sockaddr_in serveraddr;
        int sockfd;

        //1.创建用于连接服务器的sockfd
        sockfd = socket(AF_INET, SOCK_STREAM, 0);

        //2.初始化服务器端的地址信息,主机序转换为网络序
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);

        //3.连接服务器
        connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        while(1){
                //4.先发送,等待服务器端接受然后打印接受信息
                char buf[1024] = { 0 };
                read(STDOUT_FILENO, buf, sizeof(buf));
                send(sockfd, buf, strlen(buf), 0);
                printf("msg is sent\n");
                printf("*-------------------------------------*\n");

                //char buf[1024];
                memset(buf, '\0', sizeof(buf));
                recv(sockfd, buf, sizeof(buf), 0);
                write(STDOUT_FILENO, buf, strlen(buf));
                char dst[128];
                printf("from ip:%s\tport:%d\n", inet_ntop(AF_INET, &serveraddr.sin_addr.s_addr, dst, sizeof(dst)), ntohs(serveraddr.sin_port));
                printf("*-------------------------------------*\n");
        }

        //5.释放连接的文件描述符
        close(sockfd);

        return 0;
}

6.3运行结果

这里写图片描述这里写图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值