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. 寻址
网络进程标示由两部分组成,并且唯一确定
- 计算机的网络地址,也就是IP地址。
- 计算机的端口号。
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;
}