关于socket编程
socket在哪
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
TCP Socket编程
先看一张祖传图
两个结构体
网络中的地址包含3个方面的属性:
地址类型: ipv4还是ipv6 ip地址 端口
下面这两个结构体都是用来记录这些信息的
struct sockaddr
1 | |
sa_family
是2字节的地址家族,一般都是“AF_xxx”的形式,它的值包括三种:AF_INET,AF_INET6和AF_UNSPEC。
如果指定AF_INET,那么函数就不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。
AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回
通常用的都是AF_INET
sa_data
包含套接字中的目标地址和端口信息
struct sockaddr_in
in代表Internet
由于在sockaddr 中, 我们把目标port和IP都放在一起了, 就显得很难受, sockaddr_in把他分开了
sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)
1 | |
区别
sockaddr用14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。
sockaddr和sockaddr_in包含的数据都是一样的。
使用上的区别:
程序员不应操作sockaddr,sockaddr是给操作系统用的
程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。
也就是,程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数
一些转换函数
本机转换
htons()–”Host to Network Short”
htonl()–”Host to Network Long”
ntohs()–”Network to Host Short”
ntohl()–”Network to Host Long”
为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序
处理IP地址
给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr(“192.168.0.2”);
其反函数是inet_ntoa (network to ascii),可以把一个in_addr类型转换为一个字符串
需要注意的是inet_ntoa()将结构体in-addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址, 你需要保存这个IP地址,使用strcopy()函数来指向你自己的字符 指针
创建Socket套接字
1 | |
socketType
addressFamily 应该设置成 “AF_INET”然后
参数 type 告诉内核 是 SOCK_STREAM (TCP) 类型还是 SOCK_DGRAM (UDP) 类型
把 protocol 设置为 “0”
举个例子
1 | |
服务端
bind()
1 | |
如果未发生错误, 绑定将返回零。 否则,它返回SOCKET_ERROR,可以通过调用 WSAGetLastError 来检索特定的错误代码。
举个例子
1 | |
不要采用小于 1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)
listen()
调用listen后, 一个套接字会从主动连接套接字变为一个监听套接字
1 | |
关于backlog参数
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列
但是上限值是内核参数 somaxconn 的大小(大多数系统为20),也就说 accpet 队列长度 = min(backlog, somaxconn)
举个例子
1 | |
accept()
首先,服务端创建welcomsocket并bind, listen后, 调用accept函数, 它是一个阻塞函数, 会阻塞直到客户端有请求到达
客户端向服务器发起连接时, 客户端建立一个TCP套接字, 指定了服务器中welcomsocket的地址, 即服务器主机的IP地址及其套接字的端口号
accept函数的返回值是一个新的套接字描述符,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的socket,这个socket包含的是客户端的ip和port信息 。(当然这个new_socket会从welcomsocket中继承服务器的ip和port信息),而参数中的SOCKET s包含的是服务器的ip和port信息
1 | |
如果未发生错误, 则 accept 将返回一个类型为 SOCKET 的值,该值是新套接字的描述符。 此返回的值是实际连接的套接字的句柄。
否则,返回 INVALID_SOCKET 值,可以通过调用 WSAGetLastError 来检索特定的错误代码
addr返回的是一个客户端的地址, 如果对他不感兴趣, 可以设为NULL
举个例子
1 | |
客户端
connect()
1 | |
如果未发生错误, 连接 将返回零。 否则,它返回SOCKET_ERROR,可以通过调用 WSAGetLastError 来检索特定的错误代码
举个例子
1 | |
send 和 recv
用于发出和接收内容
1 | |
UDP Socket编程
相比于TCP, UDP要简单的多
主要学两个函数
先丢这里, 晚点看
函数: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
buf:读取数据的存放位置。
len:期望读取数据的字节数。
flags:读取的方式。一般设置为0,表示阻塞读取。
src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等(谁发给你的)
addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
返回值说明:
读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
注意:
由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
由于recvfrom函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转。
函数: ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
buf:待写入数据的存放位置。
len:期望写入数据的字节数。
flags:写入的方式。一般设置为0,表示阻塞写入。
dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入dest_addr结构体的长度。
返回值说明:
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
注意:
由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
由于sendto函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转