目录
前言
- 应用编程接口API和系统调用接口
- 大多数操作系统使用系统调用的机制在应用程序和操作系统之间传递控制权。对于程序员来说,每一个系统调用和一般程序中函数调用非常相似,只是系统调用是将控制权传递给操作系统。
- 当某个应用进程启动系统调用时,控制权就从应用进程传递给了系统调用接口。此接口再将控制权传递给计算机的操作系统。操作系统将此调用转给某个内部进程,并执行所请求的操作。
- 内部过程一旦执行完毕,控制权又通过系统调用接口返回给应用进程。系统调用接口实际上就是应用进程的控制权和操作系统的控制权进行转换的一个接口,即应用编程接口
API
。
socket
什么是socket
网络上的两个应用程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为socket
。socket
可以看成是用户进程与内核网络协议栈的编程接口。socket
可以看成两个进程之间通信的抽象。socket
是全双工的通信方式。
socket
不仅可以用于本机的进程间通信,还可以用于网络上不同主机(异构也可以)的进程间通信。
套接字的作用
- 当应用进程需要使用网络进行通信时就发出系统调用,请求操作系统为其创建”套接字”,以便把网络通信所需要的系统资源分配给该应用进程。
- 操作系统为这些资源的总和用一个叫做套接字描述符的号码来表示,并把此号码返回给应用进程。应用进程所进行的网络操作都必须使用这个号码。
- 通信完毕后,应用进程通过一个关闭套接字的系统调用通知操作系统回收与该”号码”相关的所有资源。
IPv4
套接口地址结构
IPv4
套接口地址结构通常也称为”网际套接字地址结构”,它以”sockadd_in
“命名。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
// sin_family:指定该地址家族,在这里必须设为AF_INET(表明使用IPv4协议)
// sin_port:端口
// sin_addr:IPv4的地址
注意:我们可以通过man 7 ip
来查看这个地址。
字节序
- 大端字节序(
Big Endian
)
最高有效位(MSB:Most Significant Bit
)存储于最低内存地址处,最低有效位(LSB:Lowest Significant Bit
)存储于最高内存地址处。 - 小端字节序(
Little Endian
)
最高有效位(MSB:Most Significant Bit
)存储于最高内存地址处,最低有效位(LSB
:Lowest Significant Bit
)存储于最低内存地址处。 - 测试字节序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
unsigned int x = 0x12345678;
unsigned char *p = (unsigned char*)&x;
printf("%0x %0x %0x %0x\n", p[0], p[1], p[2], p[3]);
// 地址上向上增长的 p[3]>p[2]>p[1]>p[0]
return 0;
}
如果输出依次为:78 56 34 12,表明为小端字节序。
如果输出依次为:12 34 56 78,表明为大端字节序。
主机字节序
不同的主机有不同的字节序,如x86
为小端字节序,ARM
字节序可配置。
网络字节序
网络字节序规定为大端字节序(低地址存高位,高地址存低位)
字节序转换函数
#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
表示host
,n
表示network
,s
代表short
,l
代表long
。
- 测试程序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
unsigned long addr = inet_addr("192.168.1.100");
printf("addr=%u\n", ntohl(addr));
return 0;
}
// 运行结果:unsigned long
// addr=3232235876 // 32位
地址转换函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
功能:将一个字符串IP地址转换为32为的网络序列IP地址。
返回值:函数成功,函数返回值非零,如果输入地址不正确会返回零。
参数1:输入参数 cp const char* 点分十进制
参数2:输出参数 in struct in_addr 无符号长整型
aton表示ascii码转换为net 也就是点分十进制转换为网络字节序
*/
int inet_aton(const char *cp, struct in_addr *inp);
/*
返回值:若字符串有效则将字符串转换为无符号长整型格式,否则为INADDR_NONE
*/
in_addr_t inet_addr(const char *cp); // 点分十进制IP转换成十进制
/*
返回值:无符号长整型转换为ascii码格式,也就是点分十进制
*/
char *inet_ntoa(struct in_addr in); // 十进制转换成点分十进制,输出是字符串格式
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
- 测试程序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
unsigned long addr = inet_addr("192.168.1.100");
printf("addr=%u\n", ntohl(addr));
struct in_addr ipaddr;
ipaddr.s_addr = addr;
printf("%s\n", inet_ntoa(ipaddr));
return 0;
}
// 运行结果
// 192.169.1.100
套接字类型
- 流式套接字(
SOCK_STREAM
)
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收 - 数据报式套接字(
SOCK_DGRAM
)
提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。 - 原始套接字(
SOCK_RAW
)
客户/服务器模型(C/S模型)
什么是客户端和服务器
客户端是服务的请求方;服务器是服务的提供方。
- 客户软件特点
- 被用户调用后运行,在打算通信时主动向远端服务器发起通信(请求服务)。因此,客户程序必须知道服务器程序的地址。
- 服务器软件特点
- 一种专门用来提供某种服务的程序,可以同时处理多个远端或者本地客户的请求。
- 系统启动后即自动调用并一直不断地运行着,被动等待并接受来自各地的客户的通信请求。因此,服务器程序不需要知道客户程序的地址。
- 一般需要强大的硬件和高级的操作系统支持。
TCP客户/服务器模型
回射客户/服务器
1. 当套接字被创建后,它的端口号和IP地址都是空的,因此应用进程要调用bind
(绑定)来指明套接字的本地地址。在服务器端调用bind
时就是把熟知端口号和本地IP
地址填写到已创建的套接字中。这就叫做把本地地址绑定到套接字。
2. 服务器在调用bind
中,还必须调用listen
(收听)把套接字设置为被动方式,以便随时接受客户的服务请求。UDP
服务器由于只提供无连接服务,不使用listen
系统调用。
3. 服务器紧接着就调用accept
(接受),以便把远端客户进程发来的连接请求(connect
)[主动方式]提取出来。系统调用accept
的一个变量就是要指明从哪个套接字发起的连接。
编程接口
socket
函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// domain:指定通信协议族(protocol family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用IPV4地址与端口号的组合。
// type:指定socket类型。流式套接字(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报套接字(SOCK_DGRAM),针对于无连接的UDP服务应用;原始套接字(SOCK_RAW)。
// protocol:协议类型。常用的协议有IPPROTO_TCP(Tcp传输协议)、IPPROTO_UDP(Udp传输协议)、IPPROTO_STCP(Stcp传输协议)、IPPROTO_TIPC(Tipc传输协议)等。
返回值:成功返回非负整数,也就是套接口描述字,简称套接字,失败返回-1.每一个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构对应的关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。
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:要绑定的地址
addrlen:地址长度
listen
函数
功能:将套接字用于监听进入的连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数1:sockfd:socket函数返回的套接字
参数2:backlog:规定内核为此套接字排队的最大连接个数
返回值:成功返回0 失败返回-1
一般来说,listen
函数应该在调用socket
和bind
函数之后,调用函数accept
之前调用。
对于给定的监听套接口,内核要维护两个队列:
1. 已由客户端发出并到达服务器,服务器正在等待完成相应的TCP
三路握手过程
2. 已完成连接的队列
博主理解:三次握手相当于第一次要登录,这时候需要走登录验证的队列;之后验证成功之后,就可以直接打开页面,而不需要跳出登录界面了。
accept
函数
功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 参数1:sockfd 服务器套接字
// 参数2:addr 将返回对等方的套接字地址
// 参数3:addrlen 返回对等方的套接字地址长度
// 返回值:成功返回非负整数(客户端文件描述符),失败返回-1
connect
函数
// 功能:用于建立与指定socket的连接
int connect(int s, const struct sockaddr * name, int namelen);
参数1:s 标识一个未连接的socket
参数2:name 指向要连接套接字的sockaddr结构体指针,也就是sockaddr中是要连接的服务器端的地址。
参数3:sockaddr 结构体的字节长度
read
函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数1:文件描述符
参数2:读取到的字节数放置的地址buf
参数3:请求从文件描述符fd读取的字节数,读取到buf
返回值:成功,返回读取的字节数;如果缓冲区中的字节数不够,读取的字节数可能比count个字节小少。错误,返回-1,同时errno将会被设置。同时文件的读写位置向后移。
write
函数
功能:从buffer中写count个字节到打开的文件中(fd表示打开的文件)
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数1:文件描述符
参数2:将要被写出到其他文件的缓冲区内容
参数3:count表示写出字节数
返回值:成功,写出的字节数返回(0表示没有可以写的了),如果写出的字节数比count小,可能是buf中没有count个字节;错误,返回-1.
write
成功返回,只是buf
中的数据被复制到内核中的TCP
发送缓冲区。置于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。
编程代码
代码功能:同时开启服务器端程序和客户端程序,客户端发送的数据会在服务器端显示出来;同时服务器端会把客户端发送的数据发回给客户端,然后客户端再显示出来。
echosvr.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main(void)
{
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机的任意地址 网络字节序
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
ERR_EXIT("bind");
}
if(listen(listenfd, SOMAXCONN) < 0)
{
ERR_EXIT("listen");
}
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
// conn表示已连接套接字,accept是被动套接字,也就是阻塞等待客户端来连接
{
ERR_EXIT("accept");
}
char recvBuf[1024];
while(1)
{
memset(recvBuf, 0, sizeof(recvBuf));
int ret = read(conn, recvBuf, sizeof(recvBuf));
fputs(recvBuf, stdout);
// 从客户端读数据,然后通过标准输出显示出来
write(conn, recvBuf, ret);
}
close(conn);
close(listenfd);
return 0;
}
// 被动套接字 accept
// 主动套接字 connect
echocli.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main(void)
{
int sock;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock <0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器端地址
if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
// connect是主动套接字,会指定将要通信的服务器端地址
{
ERR_EXIT("connect");
}
char sendBuf[1024] = {0};
char recvBuf[1024] = {0};
while(fgets(sendBuf, sizeof(sendBuf), stdin) != NULL)
{
write(sock, sendBuf, strlen(sendBuf));
read(sock, recvBuf, sizeof(recvBuf));
fputs(recvBuf, stdout);
memset(sendBuf, 0, sizeof(sendBuf));
memset(recvBuf, 0, sizeof(sendBuf));
}
return 0;
}
// 被动套接字 accept
// 主动套接字 connect
总结
socket
其实相当于两部手机bind
相当于输入手机号- 客户端拨号相当于
connect
,服务器的持续监听是否有客户端连接 - 服务器端接受客户端的拨号信息(
accept
),这时候经历了TCP/IP
三次握手协议建立了可靠连接。把可靠连接放置到另外一个队列,并提供标识。 - 利用第4步的连接标识符就可以实现通信(
read
)和(write
)。