网络地址数据结构问题
首先,先来说网络中的编程地址,不知有没有人发现在我们网络编程中不仅仅只有一个地址数据结构,而且很多时候我们在调用网络接口的时候还要强制转换参数的类型。对,我说的就是数据结构sockaddr和sockaddr_in这两货,而如果你足够仔细的话,你会发现,编程中我们使用的大部分是sockaddr_in,但是我们调用的网络接口却几乎都是(不知道有没有不是的啊,我木有去调查)sockaddr类型的,为什么?下面先来看看这两种数据结构的定义,如下所示:
/** * 通用网络地址结构 */ struct sockaddr { __uint8_t sa_len; /* 01 byte total length */ sa_family_t sa_family; /* 02 byte [XSI] address family */ char sa_data[14]; /* 14 byte [XSI] addr value (actually larger) */ }; /** * Socket address, internet style. * 我们一般使用的网络地址 * 这个是IP4类型的,定义于头文件<netinet in="" h="">中 * IP6类型的有对应的数据结构,切大小和IP4的不一样,这里就不说了 */ struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; /* 02 byte 端口 */ struct in_addr sin_addr; /* 04 byte IP地址 */ char sin_zero[8]; /* 08 byte 保留字段 */ };
从上述的代码的定义以及注释我们可以发现,sockaddr是通用的地址数据结构,最后的14个字节是保留的,而sockaddr_in是使用了sockaddr_in的后面的14字节的一种结构,两个数据结构的大小一样,只是sockaddr_in定义了sockaddr未定义的一些字节而已。这样做我想是因为想让系统平台自己决定后面的14个字节的使用方式吧,但是又给出了统一的接口,上述的实现是XSI实现的版本。
因此,我们平时声明sockaddr_in这样类型的地址结构,然后初始化该结果,最后转换为sockaddr这样类型的参数传到函数里面去使用。
TCP网络通信过程
服务器:TCP服务器端首先需要使用socket函数创建一个socket_server,然后把已经初始化的服务器地址信息使用bind函数绑定到socket_server。绑定成功后,服务器端使用listen进入监听状态,然后服务器调用accept函数进入等待客户端连接的状态,此时程序会在调用acept的地方阻塞,直到有客户端请求连接的时候,才会使得程序继续执行。此时服务器端可以使用函数accept返回的与该该客户端通信的socket_client与客户端通信,如果accept的地址参数不为空,还可以获取到客户端的地址信息。与客户端通信的可以使用read和write函数,当需要结束与该客户端的通信的时候,可以使用close关闭socket_client,断开与客户端的连接。最后程序退出的时候,别忘记把之前的监听套接字socket_server给close掉。
客户端:客户端相对于服务器端要简单点,没有所谓的监听和接受连接请求,但是有请求连接部分,下面来说说客户端是如何与服务器通信的。首先,客户端使用socket创建一个通信套接字,然偶使用服务器的地址和端口信息初始化通信地址结构,接着使用创建成功的套接字以及初始化的通信地址信息通过connect函数请求与服务器连接,如果连接成功,那么此后就可以使用该套接字和服务器通信了,也可以使用read和write函数获取和发送消息,当不需要通信的时候,客户端调用close关闭套接字,断开与服务器的连接。
其实,有个流程图还是很简单的,如下所示:
主要网络编程函数
- socket
int socket(int domain, int type, int protocol);
根据指定的地址族、数据类型和协议来分配一个socket的描述字及其所用的资源。
domain : 协议族。 常用的有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE其中AF_INET代表使用ipv4地址
type : socket类型。 常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等
protocol : 协议。 吃常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等
- bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
把一个地址族中的特定地址信息和socket绑定
sockfd : socket描述字,也就是socket引用
addr : 要绑定给sockfd的协议地址
addrlen : 地址的长度
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
- listen
int listen(int sockfd, int backlog);
服务器启动监听socket
sockfd : 要监听的socket描述字
backlog : 相应socket可以排队的最大连接个数
- accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP服务器监听到客户端请求之后,调用accept()函数处理客户端的连接请求,成功后返回与该客户端通信的套接字
sockfd : 服务器的socket描述字
addr : 客户端的socket地址
addrlen : socket地址的长度
- connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端根据服务器地址信息请求连接服务器
sockfd : 客户端的socket描述字
addr : 服务器的socket地址
addrln : socket地址的长度
- read
ssize_t read(int fd, void *buf, size_t count);
读取socket对应的可用的内容,即接收通信信息
fd : socket描述字
buf :缓冲区
count :缓冲区长度
- write
ssize_t write(int fd, const void *buf, size_t count);
向socket写入内容,其实就是发送内容
fd :socket描述字
buf :缓冲区
count :缓冲区长度
- close
int close(int fd);
关闭套接字fd,结束当前的TCP连接
代码示例
服务器端
// // main.cpp // TCPServer // // Created by God Lin on 15/1/14. // Copyright (c) 2015年 arbboter. All rights reserved. // #include <sys/socket.h> #include <unistd.h> #include <sys/types.h> #include <netdb.h> /* sockaddr_in 声明 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> /* inet_ntoa声明 */ void start_server(unsigned int port); int main(int argc, const char * argv[]) { start_server(22221); return 0; } void start_server(unsigned int port) { int fd_server = socket(AF_INET, SOCK_STREAM, 0); // set address sockaddr_in addr_server; memset(&addr_server, 0, sizeof(addr_server)); addr_server.sin_family = AF_INET; addr_server.sin_addr.s_addr = htonl(INADDR_ANY); addr_server.sin_port = htons(port); // socket bind with address if(bind(fd_server, (struct sockaddr*)&addr_server, sizeof(struct sockaddr)) == -1) { printf("bind socket failed\n"); return; } // server socket start list, waitting client to connect // 这个client_max是指同时连接数 if(listen(fd_server, 10) == -1) { printf("start listen socket failed\n"); return; } int fd_client = -1; sockaddr_in addr_client; socklen_t addr_len = 0; int nConnect = 10; while (--nConnect>0) { // 必须设置长度,否则失败的! addr_len = sizeof(struct sockaddr_in); // 商量好先读取客户端发送的信息,然后然后返回给客户端一个随机数字 fd_client = accept(fd_server, (struct sockaddr*)&addr_client, &addr_len); if(fd_client == -1) { printf("accept socket failed\n"); return; } // create a thread to talk with client printf("recived connection from [%s] : ", inet_ntoa(addr_client.sin_addr)); // 读取消息 char buf[512] = {0}; read(fd_client, buf, sizeof(buf)); printf("%s\n", buf); // 返回消息 sprintf(buf, "magic number is : %d", rand()%1000); write(fd_client, buf, strlen(buf)); // 关闭本次通信 close(fd_client); } close(fd_server); }
客户端代码
// // main.cpp // TCPServer // // Created by God Lin on 15/1/14. // Copyright (c) 2015年 arbboter. All rights reserved. // #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netdb.h> #include <string.h> #include <arpa/inet.h> #include <string.h> void start_client(const char* server_addr, int port); int main() { start_client("127.0.0.1", 22221); return 0; } void start_client(const char* server_addr, int port) { int sockfd_server; struct sockaddr_in addr_server; // create socket sockfd_server = socket(AF_INET, SOCK_STREAM, 0); if(sockfd_server == -1) { printf("init socket failed\n"); return; } // set server address memset(&addr_server, 0, sizeof(addr_server)); addr_server.sin_family = AF_INET; addr_server.sin_addr.s_addr = inet_addr(server_addr);; addr_server.sin_port = htons(port); // connect server if(connect(sockfd_server,(struct sockaddr *)&addr_server,sizeof(struct sockaddr))==-1) { printf("connect server failed\n"); return; } char buf[512] = {0}; sprintf(buf, "Hi, i'm hello!"); write(sockfd_server, buf, strlen(buf)); // 获取服务器返回信息 size_t nRead = read(sockfd_server, buf, sizeof(buf)); buf[nRead] = '\0'; printf("recived from server : %s", buf); close(sockfd_server); }