网络地址数据结构问题
首先,先来说网络中的编程地址,不知有没有人发现在我们网络编程中不仅仅只有一个地址数据结构,而且很多时候我们在调用网络接口的时候还要强制转换参数的类型。对,我说的就是数据结构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
根据指定的地址族、数据类型和协议来分配一个socket的描述字及其所用的资源。int socket(int domain, int type, int protocol);
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
把一个地址族中的特定地址信息和socket绑定int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd : socket描述字,也就是socket引用
addr : 要绑定给sockfd的协议地址
addrlen : 地址的长度
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
- listen
服务器启动监听socketint listen(int sockfd, int backlog);
sockfd : 要监听的socket描述字
backlog : 相应socket可以排队的最大连接个数
- accept
TCP服务器监听到客户端请求之后,调用accept()函数处理客户端的连接请求,成功后返回与该客户端通信的套接字int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
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
读取socket对应的可用的内容,即接收通信信息ssize_t read(int fd, void *buf, size_t count);
fd : socket描述字
buf :缓冲区
count :缓冲区长度
- write
向socket写入内容,其实就是发送内容ssize_t write(int fd, const void *buf, size_t count);
fd :socket描述字
buf :缓冲区
count :缓冲区长度
- close
关闭套接字fd,结束当前的TCP连接int close(int fd);
代码示例
服务器端
//
// 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);
}