文章目录
关于 socket的编程,有很多知识。本文讲述一下这部分比较基础的点。
1. 关键的结构体
在说sockaddr_in
之前,我觉得有必要列出来sockaddr
这个结构体。
1.1. sockaddr
sockaddr
是通用的socket地址,其定义在#include <sys/socket.h>
中。
struct sockaddr
{
sa_family_t sin_family; //地址族,一般使用AF_INET
char sa_data[14]; //14 bytes 协议地址
};
此结构体用作bind,connect, recvfrom, sendto
等函数的参数中,以指明地址信息。但一般编程并不会使用它,而是使用等价的结构体sockaddr_in
。
1.2. sockaddr_in
看过一点相关代码的朋友,一定对这组结构体不陌生:
struct sockaddr_in
{
sa_family_t sin_family; //地址族,一般使用AF_INET
unit16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //一般不使用
};
而其中包含的结构体定义为:
struct in_addr
{
In_addr_t s_addr; //32位IPv4地址
};
sockaddr_in
在头文件#include<netinet/in.h>
或#include <arpa/inet.h>
中定义,该结构体把port
和addr
分开储存在两个变量中,这种方式解决了sockaddr
的缺陷。
1.3. 说明
sin_family
是地址族(Address Family),其格式位AF_XXXX
,对于socket编程,全部使用AF_INET
,这代表了TCP\UDP
。sin_port
和sin_addr(s_addr)
都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。sin_zero
虽然不怎么使用,但也要明白是干嘛的。其作用是为了使sockaddr
和sockaddr_in
两个数据结构大小保持相同而保留的空字节。sockaddr_in
和sockaddr
是并列的结构,指向sockaddr_in
的结构体的指针也可以指向sockaddr
的结构体,并代替它。也就是说,你可以使用sockaddr_in
建立你所需要的信息, 然后用进行类型转换就可以了。
1.4. 初始化结构体
#define MY_PORT 8888
int mySocket; /*服务器套接字文件描述符*/
struct sockaddr_in mySocket_addr;
/*初始化地址*/
bzero(&mySocket_addr, sizeof(struct sockaddr_in)); /*清零*/
mySocket_addr.sin_family = AF_INET; /*AF_INET协议族*/
mySocket_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
mySocket_addr.sin_port = htons(MY_PORT); /*定义的端口*/
1.5. 初始化的注释
1.5.1. 端口号
端口是很重要的一个概念,我这里端口号8888
只是我随意写的一个例子。端口是一种抽象的结构,包括数据结构和I/O缓冲区。
应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。
这是我找到的一个讲的很好的博文:TCP/UDP共用端口问题
1.5.2. htons(), htonl(), ntohs(), ntohl()
函数说明
初始化的时候,用到了这两个函数htons
和htonl
,那么本节就说下这两个以及相关的函数。
函数 | 说明 |
---|---|
uint16_t htons(uint16_t netshort) |
此函数将将主机的无符号短整形数转换成网络字节 顺序字节顺序 ,返回值为一个网络字节顺序的值 |
uint32_t htonl(uint32_t hostlong) |
将一个32位数从主机字节顺序转换成网络字节顺序, 返回一个网络字节顺序的值 |
uint16_t ntohs(uint16_t netshort) |
将一个16位数由网络字节顺序转换为主机字节顺序, 返回一个以主机字节顺序表达的数 |
uint32_t ntohl(uint32_t netlong) |
将一个32位数从网络字节顺序转换为主机字, 返回一个以主机字节顺序表达的数 |
我在进行初始化,自然要转地址位为网络地址,所以使用htons
和htonl
。而正如上面所说,sin_port
是16位,s_addr
是32位,因而如此分别使用:
mySocket_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
mySocket_addr.sin_port = htons(MY_PORT); /*定义的端口*/
在此需要说明,INADDR_ANY是指地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。当初始化,不确定地址时候,可以使用。如此将开启计算机所有IP地址/网卡等待接收信号。
打个比方,比如你的机器有三个ip:192.168.1.1
202.202.202.202
61.1.2.3
。如果你预设的值为server.sin_addr.s_addr=inet_addr("192.168.1.1");
,然后监听100端口。这时其他机器只有连接 192.168.1.1:100
才能成功。 连接202.202.202.202:100
和61.1.2.3:100
都会失败。 但是如果server.sin_addr.s_addr=htonl(INADDR_ANY);
的话,无论连接哪个ip都可以连上的。
1.5.3. inet_ntoa()
和 inet_addr()
说到地址转化函数,这两个也需要提一下。
char *inet_ntoa(struct in_addr in)
将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
in_addr_t inet_addr(const char *cp)
参数为一个点分十进制的IP地址,如果正确执行将返回一个无符号长整数型数。
如果传入的字符串不是一个合法的IP地址,将返回INADDR_NONE。
2. 建立连接并且通讯
这一节将说一下建立连接和通讯的相关函数。
2.1. 创建套接字: socket()
2.1.1. 函数说明
int socket( int af, int type, int protocol)
- af:一个地址描述。仅支持
AF_INET
格式,即上面所说的mySocket_addr.sin_family = AF_INET
。 - type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,
SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等等。 - protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0(一般都用0)。常用的协议有,
IPPROTO_TCP
、IPPROTO_UDP
、IPPROTO_STCP
、IPPROTO_TIPC
等,它们分别对应TCP传输协议
、UDP传输协议
、STCP传输协议
、TIPC传输协议
。
2.1.2. 举例
TCPSocket = socket(AF_INET, SOCK_STREAM, 0) //TCP面向字节流(SOCK_STREAM)
UDPSocket = socket(AF_INET, SOCK_DGRAM, 0) //UDP面向报文(SOCK_DGRAM)
2.1.3. 适用范围
UDP/TCP 的 服务端/客户端 均需要
2.2. 绑定端口/IP: bind()
2.2.1. 函数说明
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
- sockfd是用
socket()
函数创建的文件描述符。 - my_addr是指向一个结构为
sockaddr
参数的指针,sockaddr
中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr
中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。 - addrlen是
my_addr
结构的长度,可以设置成sizeof(struct sockaddr)
。使用sizeof(struct sockaddr)
来设置套接字的类型和其对已ing的结构。 - 返回值为0时表示绑定成功,-1表示绑定失败,errno的错误值请自行百度。
2.2.2. 举例
if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("bind error");
return 1;
}
2.2.3. 适用范围
适用于TCP\UDP的服务端。
这里解释下为何只用于服务端:
首先说明原理,bind
函数就是绑定, 将一个socket
绑定到一个地址上, 也可以这么说:bind
函数对一个socket
进行命名(注意:socket名称包括三要素: 协议, ip, port)
-
对于TCP的服务端,
bind()
函数是一定需要的。可以做个小实验试下,如果不使用bind()
,那么客户端在进行连接时候就会报错Connection refused
。这很好理解,TCP是面向连接的,服务器是时时在监听(listen
)有没有客户端的连接。如果服务器不绑定IP和端口的话(bind
),客户端上线的时候怎么连到服务器呢。所以服务器要绑定IP和端口,而客户端就不需要了。 -
对于TCP的客户端,
bind()
一般是不需要的。客户端上线是主动向服务器发出请求的,因为服务器已经绑定了IP和端口,所以客户端上线的就向这个IP和端口发出请求,这时因为客户开始发数据了(发上线请求),系统就给客户端分配一个随机端口,这个端口和客户端的IP会随着上线请求一起发给服务器, 服务器收到上线请求后就可以从中获起发此请求的客户的IP和端口,接下来服务器就可以利用获起的IP和端口给客户端回应消息了。总之一句话:客户端是主动连接(connect
), 而服务器是等待接受连接(accept
)。 -
对于UD