提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:
前言
此文章是学习腾讯课堂零声教育C/C++linux服务器开发课程的总结笔记
网络IO-socket篇
一 、什么是IO
IO的简写是输入输出,操作系统层级以上的IO就是对FD(文件描述符)的操作,操作网络socket就叫网络IO,操作磁盘文件就叫磁盘IO,对输入输出的总称就叫做IO。
二、服务器网络IO模型
下图就是自己画的网络IO模型,大部分的服务器的底层都是基于此网络IO模型
客户端通过connect()发起连接请求到服务器的listenFd,服务器的listenFd接收到了客户端的连接请求事件后,通过accept()返回一个clientFd与客户端的建立网络连接,这样客服端和服务端就可以通过clientFd进行网络通信了。
三、什么是socket
TCP/IP协议是在内核中实现的,操作系统需要一组系统调用,使得应用程序能访问这些协议提供的服务,这组系统调用的API就叫做socket。
socket翻译过来就是插座的意思,由插和座组成,插就是FD的部分(可以进行数据的IO操作),座就是网络通信控制块(网络属性)。
四、socket API
1.inux网络地址API
主要从3个方面介绍linux网络地址API,然后介绍IP地址转换函数
1.字节序问题
socket地址包含了一个IP地址和端口对(ip, port),它唯一地表示了使用TCP通信的一端。
首先要了解主机字节序与网络字节序,CPU累加器一次能转载4字节即一个整数,这4个字节在内存中的排列顺序将影响他被累加器转载成的整数值,这样就有2种字节序,分别为大端字节序和小端字节序,大端字节序指一个整数的高位字节(23~31bit)存储在内存的低地址处,低位字节存(0 ~7bit)储在内存的高地址处,小端字节序则相反。
小端字节序被称为主机字节序,大端字节序被称为网络字节序。
可以通过C语言的union结构体测试机器的字节序:
typedef union u_CheckByteOrder{
short value;
char unionBytes[sizeof(short)];
} CheckByteOrder;
void ByteOrder(void)
{
CheckByteOrder test;
test.value = 0x0102;
if ((test.unionBytes[0] == 1) && (test.unionBytes[1] == 2)) {
printf("big endian\n");
}
else if ((test.unionBytes[0] == 2) && (test.unionBytes[1] == 1)) {
printf("little endian\n");
}
else {
printf("unknown...\n");
}
}
所以每次对网络发送数据和从网络接收数据都要对字节序进行转换,不然解析出来的数据就会出错。
linux提供了4个函数完成主机序和网络字节序的转换:
extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
extern uint16_t ntohs (uint16_t __netshort)__THROW __attribute__ ((__const__));
extern uint32_t htonl (uint32_t __hostlong)__THROW __attribute__ ((__const__));
extern uint16_t htons (uint16_t __hostshort) __THROW __attribute__ ((__const__));
函数的含义很明确,htonl就表示“host to network long”,即将长整型(32bit)的主机字节序转换为网络字节序,htonl常用来转换ip地址,htons常用来转换端口。
2.通用socket地址
网络编程接口中表示socket地址的是结构体socksddr,其定义如下:
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
__SOCKADDR_COMMON 成员是地址族类型变量,sa_data成员用于存放地址值。
3.专用socket地址
socksddr很明显不好用,设置与获取ip地址和端口号就需要执行繁琐的位操作,所以linux提供了专用的结构体:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
sockaddr_in是用于IPv4的,本文不讨论IPv6,有了sockaddr_in我们就可以方便的设置端口和IP了。
所有专用的socket地址类型变量在实际的使用中都需要转换为通用的socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr。
IP地址转换函数
我们喜欢用可读性良好的字符串表示ip地址,比如用点分十进制表示,linux提供了三个函数用于点分十进制字符串表示的IPV4地址与网络字节序整数表示的转换:
#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton( const char* cp, struct in_addr* inp);
char* inet_ntoa(struct in_addr in );
inet_addr函数将用点分十进制字符串表示的ipv4地址转化为用网络字节序整数表示的IPV4地址。失败时返回INADDR_NONE;
inet_aton函数完成和inet_addr一样的功能,但是将转换结果存储与参数inp指向的地址结构中。成功时返回1,失败时返回0;
inet_ntoa函数则是反过来,把网络字节序整数表示的IPV4地址转换为点分十进制字符串表示的IPV4地址。需要注意的是该函数内部用一个静态变量保存转化的结果,该函数返回的值指向静态内存,因此inet_ntoa函数是不可重入的。
2.创建socket
unix/linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。通过socket系统调用创建一个socket。
#include <sys/socket.h>
#include <sys/types.h>
int socket(int domain, int type, int protocal);
domain参数指明使用哪个底层协议族。对TCP/IP而言设置为AF_INET或PF_INET都可以,表示的是IPV4协议。
type参数指定服务类型,服务类型主要油SOCK_STREAM(流服务)和SOCK_UGRAM(数据报服务),SOCK_STREAM采用的TCP协议,SOCK_UGRAM采用的是UDP协议。
protocal参数默认设置为0就可以了。
socket系统调用成功返回一个socket文件描述符,失败则返回-1,并设置errno。
3.命令socket
我们需要将一个socket与socket地址绑定叫做给socket命名。只有命名后客户端才知道怎么连接它。客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。
命名socket的系统调用是bind:
#include <sys/socket.h>
#include <sys/types.h>
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
bind 将my_addr所指的socket地址分配给未命名的sockfd,addrlen指出该socket地址的长度。
bind 系统调用成功返回0,失败则返回-1,并设置errno。
4.监听socket
socket被命名后还不能马上接收客户端的连接,需要使用listen系统调用创建一个监听队列用于存放待处理的客户连接:
#include <sys/socket.h>
int listen(int sockfd, int backlog)
sockfd指定被监听的socket,backlog参数指定内核监听队列的最大长度,监听队列长度如果超过backlog,服务器将不受理新的客户连接。
listen系统调用成功返回0,失败则返回-1,并设置errno。
5.接受连接
accept系统调用从listen监听队列中接收一个连接:
#include <sys/socket.h>
#include <sys/types.h>
int accept (int sockfd, struct sockaddr * addr, socklen_t * addr_len);
sockfd参数是执行listen调用的监控socket, addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addr_len参数指出。
accept 成功时返回一个新的连接socket,该accept 唯一的标识了被接受的这个连接,服务器可以通过读写这个socket来与接受连接对应的客户端通信。
accept 失败则返回-1,并设置errno。
6.发起连接
服务器通过listen调用被动的接受连接,那么客户端则通过connect系统调用主动发起对服务端的连接:
#include <sys/socket.h>
#include <sys/types.h>
int connect (int sockfd, struct sockaddr * addr, socklen_t addr_len);
sockfd参数是由socket系统调用返回的一个socket,addr参数指明服务器监听的socket地址,addr_len指定这个地址的长度。
connect成功时返回0。一旦成功建立连接,sockfd就唯一的表示了这个连接,客户端可以通过读写这个sockfd与服务器通信。
connect失败则返回-1,并设置errno。
7.关闭连接
关闭一个连接实际上就是关闭该链接对应的socket,,通关关闭普通文件描述符的系统调用来完成:
#include <unistd.h>
int close(int fd);
fd参数是待关闭的socket
8.数据读写
对文件操作的read和write同样适用于socket,但是socket编程接口提供了专门用于socket数据读写的系统调用,它们增加了对数据读写的控制,其中御用TCP流数据读写的系统调用是:
#include <sys/socket.h>
#include <sys/types.h>
ssize_t send (int sockfd, const void *buf, size_t n, int flags);
ssize_t recv (int sockfd, void *buf, size_t n, int flags);
recv 接收sockfd上的数据,buf和n参数分别指示读缓冲区的位置和大小,flags参数通常设置为0就可以了,recv成功时返回读取到数据的长度,recv返回0表示通信对方已经关闭了连接,recv出错时返回-1,并设置errno。
send往sockfd上写入数据,buf和n分别指写缓冲区位置和大小,send成功时返回实际写入数据的长度。send出错时返回-1,并设置errno。
9.服务端代码示例
目前只演示建立一个客户端连接的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv 接收的最大数据长度
char buff[MAX_SIZE]; // send和recv的缓冲区
volatile int run = true;
int main(int argc, char const *argv[])
{
int ret = 0;
int ListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (ListenFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
ret = bind(ListenFd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if ( ret == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
ret = listen(ListenFd, 10);
if (ret == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd;
clientfd = accept(ListenFd, (struct sockaddr *)&client, &len);
if (clientfd < 0) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
int recNum; // 指示recv接收到的数据长度
while(run) {
recNum = recv(clientfd, buff, MAX_SIZE, 0);
if (recNum > 0) {
buff[recNum] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, recNum, 0);
} else if (recNum == 0) { // 客户端断开网络连接
close(clientfd);
break;
}
}
return 0;
}
10.客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv 接收的最大数据长度
char buff[MAX_SIZE]; // send和recv的缓冲区
int main(int argc, char const *argv[])
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("192.168.83.110");
servaddr.sin_port = htons(9999);
int sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
int ret = 0;
ret = connect(sockFd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1) {
printf("connect socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
const char* sendData = "hello,world!";
send(sockFd, sendData, strlen(sendData), 0);
int recvLength = 0;
recvLength = recv(sockFd, buff, MAX_SIZE, 0);
if (recvLength > 0) {
buff[recvLength] = '\0';
printf("recv msg from client: %s; recv length is:%d\n", buff, recvLength);
}
close(sockFd);
return 0;
}
总结
本文是自己对网络IO的学习笔记,主要总结了网络IO的概念,服务器网络IO模型,TCP/IP协议族在linux上的实现(socket API ),最后给出了一个服务器-客户端单连接的代码案例。