网络IO与socket

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[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 ),最后给出了一个服务器-客户端单连接的代码案例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值