1. 什么是socket?
- socket可以看成是用户进程与内核网络协议栈的编程接口;
- socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信;
- 每一个套接口有一个地址属性,
2. IPv4套接口
- IPv4套接口地址结构通常也称为网际套接字地址结构,它以
sockaddr_in
命名,定义在头文件<netinet/in.h>
中:
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port; /* 端口*/
struct in_addr sin_addr; /* Internet 地址 */
char sin_zero[8];
};
-
sin_len
:整个sockaddr_in
结构体的长度,有些没有这个; -
sin_family
:指定该地址家族,在这里必须设为AF_INET
(AF_INET6
表示IPv6地址族) -
sin_port
:端口,无符号16位整数,最大值为65535 -
sin_addr
:IPv4的地址,无符号32位整数 -
sin_zero
:暂不使用,一般将其设置为0
通过man
帮助手册参看地址结构:man 7 ip
3. 通用的地址结构
- 通用地址结构用来指定与套接字关联的地址:
struct sockaddr {
uint8_t sin_len;
sa_family_t sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
-
sin_len
:整个sockaddr_in
结构体的长度,有些没有这个; -
sin_family
:指定该地址家族 -
sa_data
:由sin_family
决定它的形式
4. 网络字节序
- 大端字节序(Big Endian):最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址处;
- 小端字节序(Little Endian):最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址处;
- 主机字节序:不同的主机有不同的字节序;
- 网络字节序:网络字节序规定为大端字节序;
socket编程支持异构系统,不同的操作系统的字节序可能不相同,因此传输过程中统一成网络字节序
测试本机操作系统是大端还是小端:
#include <stdio.h>
using namespace std;
int main()
{
unsigned int a = 0x12345678;
unsigned char* p = (unsigned char*)&a;
printf("%0x, %0x, %0x, %0x\n", p[0], p[1], p[2], p[3]);
return 0;
}
5. 字节序转换函数
uint16_t htons(uint16_t hostshort)--"Host to Network Short"
uint32_t htonl(uint32_t hostlong)--"Host to Network Long"
uint16_t ntohs(uint16_t netshort)--"Network to Host Short"
uint32_t ntohl(uint32_t netlong)--"Network to Host Long"
6. 地址转换函数
点分十进制地址与32为整数地址转换
头文件:
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char* cp, struct in_add* inp);
in_addr_t inet_addr(const char* cp);
// 将点分十进制地址转换为32位整数
char* inet_ntoa(struct in_addr in);
// 将地址结构(32位整数)转换为点分十进制
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
unsigned long addr = inet_addr("192.168.0.100"); // 将点分十进制地址转换为32位整数
printf("addr = %u\n", ntohl(addr)); // 将32位整数转化为网络字节序
struct in_addr ipaddr;
ipaddr.s_addr = addr;
printf("%s\n", inet_ntoa(ipaddr)); // 将地址结构(32位整数)转换为点分十进制
return 0;
}
7. 套接字类型
- 流式套接字
SOCK_STREAM
(TCP协议):提供面向连接的,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收 - 数据报式套接字
SOCK_DGRAM
(UDP协议):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱 - 原始套接字
SOCK_RAW
:将应用层的数据跨越传输层,直接封装成网路层的数据
8. TCP客户/服务器模型(C/S模型)
C/S模型9. 回射客户/服务器模型
回射客户/服务器模型10. socket函数
- 头文件:
<sys/socket.h>
- 功能:创建一个套接字用于通信
- 原型:
int socket(int domain, int type, int protocol);
domain
:指定通信协议族(protocol family)
type
:指定socket类型,流式套接字SOCK_STREAM
,数据报套接字SOCK_DGRAM
,原始套接字SOCK_RAW
protocol
:协议类型 - 返回值:成功返回非负整数,它与文件描述符类似,我们将它称为套接字描述字,简称套接字。失败返回-1。
在man
帮助手册输入man socket
即可获得帮助文档
// 创建套接字
int listenfd;
if ( (listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) // PF_INET(AF_INET):IPv4 Internet protocols, SOCK_STREAM:流式套接字
{
ERR_EXIT("socket");
}
11. bind函数
- 头文件:
<sys/socket.h>
- 功能:绑定一个本地地址到套接字
- 原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
sockfd
:socket函数返回的套接字
addr
:要绑定的地址
addrlen
:地址长度 - 返回值:成功返回0, 失败返回-1
在man
帮助手册输入man bind
即可获得帮助文档
// 地址的初始化
struct sockaddr_in servaddr; // IPv4的地址结构
memset(&servaddr, 0, sizeof(servaddr)); // 初始化地址
servaddr.sin_family = AF_INET; // 地址族
servaddr.sin_port = htons(5188); // 指定端口号,并将端口号转化为2个字节的网络字节序
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");
}
12. listen函数
- 头文件:
<sys/socket.h>
- 功能:将套接字用于监听进入的连接
- 原型:
int listen(int sockfd, int backlog);
- 参数:
sockfd
:socket函数返回的套接字 -
backlog
:规定内核为此套接字排队的最大连接个数
调用listen
函数后套接字变为被动套接字
- 被动套接字:接收连接(调用
accept
函数接收连接) - 主动套接字(默认):用来发起连接(调用
connect
3函数发起连接)
// 监听
if( listen(listenfd, SOMAXCONN) < 0 )
{
ERR_EXIT("listen");
}
- 一般来说,
listen
函数应该在调用socket
函数和bind
函数之后,调用accept
函数之前调用 - 对于给定的监听套接口,内核需要维护两个队列:1.已由客户发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程 2. 已完成连接的队列
13. accept函数
- 头文件:
<sys/socket.h>
- 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
- 原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
sockfd
:服务器套接字
addr
:将返回对等方的套接字地址
addrlen
:返回对等方的套接字地址长度 - 返回值:成功返回非负整数,失败返回-1
// 接收
struct sockaddr_in peeraddr; // 对方的地址
socklen_t peerlen = sizeof(peeraddr);
int conn; // 定义已连接套接字
if( accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen) < 0 )
{
ERR_EXIT("accept");
}
14. connect函数
- 头文件:
<sys/socket.h>
- 功能: 建立一个连接至addr所指定的套接字
- 原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:未连接的套接字
addr
:要连接的套接字地址
addrlen
:地址长度 - 返回值:成功返回0, 失败返回-1
// 连接 connect
if( connect(sock, (struct sockaddr*)(&cliaddr), sizeof(cliaddr)) < 0 )
{
ERR_EXIT("connect");
}
15. REUSEADDR
- 服务器端尽可能使用
REUSEADDR
- 在绑定之前尽可能调用
setsockopt
来设置REUSEADDR套接字选项 - 使用REUSEADDR选项可以使得不必等待
TIME_WAIT
状态消失就可以重启服务器
// 设置地址重复利用 REUSEADDR
int on = 1;
if( (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) )
{
ERR_EXIT("setsockopt");
}
16. process-per-connection
- 一个连接一个进程来处理并发
- 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果。父进程用来监听,子进程用来处理通信
// 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果
// 父进程用来监听,子进程用来处理通信
pid_t pid;
while(true)
{
if( (conn = accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen)) < 0 )
{
ERR_EXIT("accept");
}
printf("ip=%s, port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); // 打印客服端发送过来的ip和port
pid = fork();
if( pid == -1 )
{
ERR_EXIT("fork");
}
else if( pid == 0 ) // 子进程来处理通信
{
close(listenfd); // 子进程不需要监听,将监听套接口关闭
do_servece(conn); // 通信处理函数
exit(EXIT_SUCCESS); // 当客户端关闭后,子进程销毁
}
else // 父进程
{
close(conn); // 父进程不需要通信,将通信套接口关闭
}
}
// 通信处理函数
void do_servece(int conn)
{
char recvbuf[1024];
while(true)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
if( ret == 0 ) // 返回值为0,表示客户端关闭
{
printf("Client close");
break; // 当客户端关闭,退出循环
}
else if( ret == -1)
{
ERR_EXIT("read");
}
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
}
17. 点对点的聊天程序实现
- 即两端能够聊天通信
- 实现原理:在服务器和客户端都创建两个进程,一个用来接收数据,一个用来发送数据
源码
18. 流协议与粘包问题
- TCP为字节流传输,无边界
网友评论