socket编程
socket与计算机网络
两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
在Unix一切皆文件哲学的思想下,socket是一种”打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个”文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
socket通信流程
流程如上图所示;
联想三次握手:
经过对比会发现socket的流程中建立连接的部分就是三次握手;
socket API与编程
服务端
创建套接字对象
1
2
3
4
5
6/*
* _domain 套接字使用的协议族信息
* _type 套接字的传输类型
* __protocol 通信协议
* */
int socket (int __domain, int __type, int __protocol) __THROW;参数:
第一个参数,协议族信息可选如下:
(在Linux系统中
AF_
和PF_
是等价的。在内核源码中net目录下面有Af_开头的一系列文件(如:Af_inet.c、Af_inet6.c、Af_unix.c等),每一个文件分别代表了一种协议族。)
地址族 | 含义 |
---|---|
AF_INET | IPv4网络协议中采用的地址族 |
AF_INET6 | IPv6网络协议中采用的地址族 |
AF_UNIX, AF_LOCAL | 本地通信中采用的UNIX协议的地址族(用的少) |
AF_PACKET | 链路层通信 |
第二个参数:套接字类型:
参考内核源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* enum sock_type - Socket types
* @SOCK_STREAM: stream (connection) socket
* @SOCK_DGRAM: datagram (conn.less) socket
* @SOCK_RAW: raw socket
* @SOCK_RDM: reliably-delivered message
* @SOCK_SEQPACKET: sequential packet socket
* @SOCK_DCCP: Datagram Congestion Control Protocol socket
* @SOCK_PACKET: linux specific way of getting packets at the dev level.
* For writing rarp and other similar things on the user level.
*
* When adding some new socket type please
* grep ARCH_HAS_SOCKET_TYPE include/asm-* /socket.h, at least MIPS
* overrides this enum for binary compat reasons.
*/
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
套接字类型 | 含义 |
---|---|
SOCKET_RAW | 原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议。 |
SOCK_STREAM | SOCK_STREAM是数据流,一般为TCP/IP协议的编程。 |
SOCK_DGRAM | SOCK_DGRAM是数据报,一般为UDP协议的网络编程; |
- 第三个参数:最终采用的协议;常见的协议有IPPROTO_TCP、IPPTOTO_UDP。如果第二个参数选择了SOCK_STREAM,那么采用的协议就只能是IPPROTO_TCP;如果第二个参数选择的是SOCK_DGRAM,则采用的协议就只能是IPPTOTO_UDP。
向套接字分配网络地址
1
2
3
4
5
6/*
* __fd:socket描述字,也就是socket引用
* myaddr:要绑定给sockfd的协议地址
* __len:地址的长度
*/
int bind (int __fd, const struct sockaddr* myaddr, socklen_t __len) __THROW;第一个参数:socket文件描述符
__fd
即套接字创建时返回的对象;第二个参数:
myaddr
则是填充了一些网络地址信息,包含通信所需要的相关信息,其结构体具体如下:1
2
3
4
5struct sockaddr
{
sa_family_t sin_family; /* Common data: address family and length. */
char sa_data[14]; /* 地址数据 */
};这里根据socket源码:
typedef unsigned short sa_family_t;
表示地址族,可选参数如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/* Supported address families. */
一般情况下会用这个
sockaddr
的变体sockaddr_in
进行字段的初始化:1
2
3
4
5
6struct sockaddr_in{
sa_family_t sin_family; //前面介绍的地址族
uint16_t sin_port; //16位的TCP/UDP端口号
struct in_addr sin_addr; //32位的IP地址
char sin_zero[8]; //不使用
}其中
in_addr
结构定义如下:1
2
3
4
5
6/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};其中
s_addr
是一种uint32_t
类型的数据,而且在网络传输时,统一都是以大端序的网络字节序方式传输数据;但我们通常习惯的IP地址是点分十进制,比如“219.228.148.169”;可以使用如下函数转化把IP地址转换成32位的整数并且进行网络字节转换:1
2
3in_addr_t inet_addr (const char *__cp) __THROW;
//或者
int inet_aton (const char *__cp, struct in_addr *__inp) __THROW; //windows无此函数如果单纯要进行网络字节序地址的转换,可以采用如下函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/*Functions to convert between host and network byte order.
Please note that these functions normally take `unsigned long int' or
`unsigned short int' values as arguments and also return them. But
this was a short-sighted decision since on different systems the types
may have different representations but the values are always the same. */
// h代表主机字节序
// n代表网络字节序
// s代表short(4字节)
// l代表long(8字节)
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)sin_zero
无特殊的含义,只是为了与下面介绍的sockaddr结构体一致而插入的成员。
在具体调用bind的函数的时候,用强制类型转换把
sockaddr_in
转化为sockaddr
即可;1
2
3struct sockaddr_in serv_addr;
...
bind(serv_socket, (struct sockaddr*)&serv_addr, sizeof(serv_addr);
进入等待连接请求状态
1
2
3
4/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;给套接字分配了所需的信息后,就可以调用
listen()
函数对来自客户端的连接请求进行监听(客户端此时要调用connect()
函数进行连接)- 第一个参数:socket文件描述符
__fd
,分配所需的信息后的套接字。 - 第二个参数:连接请求的队列长度,如果为6,表示队列中最多同时有6个连接请求。
这个函数的fd(socket套接字对象)就相当于一个门卫,对连接请求做处理,决定是否把连接请求放入到server端维护的一个队列中去。
- 第一个参数:socket文件描述符
受理客户端的连接请求
1
2
3
4
5
6
7
8
9/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);listen()
中的sock(__fd : socket对象)发挥了服务器端接受请求的门卫作用,此时为了按序受理请求,给客户端做相应的回馈,连接到发起请求的客户端,此时就需要用accept再次创建另一个套接字;函数成功执行时返回socket文件描述符,失败时返回-1。
- 第一个参数:socket文件描述符
__fd
,要注意的是这个套接字文件描述符与前面几步的套接字文件描述符不同。 - 第二个参数:保存发起连接的客户端的地址信息。
- 第三个参数: 保存该结构体的长度。
- 第一个参数:socket文件描述符
send/write发送信息
linux下面的发送函数:
1
2
3
4/* Write N bytes of BUF to FD. Return the number written, or -1.(往__fd里面写N个__buf里面的bytes;返回写入的数字或者-1)
This function is a cancellation point and therefore not marked with
__THROW. */
ssize_t write (int __fd, const void *__buf, size_t __n) ;windows下面的发送函数:
1
ssize_t send (int sockfd, const void *buf, size_t nbytes, int flag) ;
recv/read接受信息
linux下的接收函数为
1
2
3
4
5
6/* Read NBYTES into BUF from FD. Return the
number read, -1 for errors or 0 for EOF.
This function is a cancellation point and therefore not marked with
__THROW. */
ssize_t read (int __fd, void *__buf, size_t __nbytes);而在windows下的接收函数为
1
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flag) ;
关闭连接
1
2
3
4
5/* Close the file descriptor FD.
This function is a cancellation point and therefore not marked with
__THROW. */
int close (int __fd);退出连接,此时要注意的是:调用
close()
函数即表示向对方发送了EOF
结束标志信息。
客户端
服务端的socket套接字在绑定自身的IP即 及端口号后这些信息后,就开始监听端口(listen)等待客户端的连接(connect)请求,此时客户端在创建套接字后就可以按照如下步骤与server端通信:
创建套接字对象(如上)
请求连接
1
2
3
4
5
6
7
8/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);几个参数的意义和前面的accept函数意义一样。要注意的是服务器端收到连接请求的时候并不是马上调用accept()函数,而是把它放入到请求信息的等待队列中。
读写信息
关闭连接
套接字的多种可选项
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Put the current value for socket FD's option OPTNAME at protocol level LEVEL
into OPTVAL (which is *OPTLEN bytes long), and set *OPTLEN to the value's
actual length. Returns 0 on success, -1 for errors. */
extern int getsockopt (int sock, int __level, int __optname,
void *__optval, socklen_t *optlen) __THROW;
/* Set socket FD's option OPTNAME at protocol level LEVEL
to *OPTVAL (which is OPTLEN bytes long).
Returns 0 on success, -1 for errors. */
extern int setsockopt (int sock, int __level, int __optname,
const void *__optval, socklen_t __optlen) __THROW;参数:
sock
:网络文件描述符;也就是前面sock函数的返回值__level
:选项所在协议层。 可选的协议层如下:
协议层 | 功能 |
---|---|
SOL_SOCKET | 套接字相关通用可选项的设置 |
IPPROTO_IP | 在IP层设置套接字的相关属性 |
IPPROTO_TCP | 在TCP层设置套接字相关属性 |
__optname
:需要访问的选项名 (取决于level) :__optval
:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。__optlen
:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。PS:__THROW是linux平台C库才有的东西,类似于throw()
使用案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
651. closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt (s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
2. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历
TIME_WAIT的过程:
BOOL bDontLinger = FALSE;
setsockopt (s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
3. 在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒
//发送时限
setsockopt (socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt (socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
4. 在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节
(异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt (s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=32*1024;//设置为32K
setsockopt (s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
5. 如果在发送数据的时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响
程序的性能:
int nZero=0;
setsockopt (socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
6. 同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt (socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
7. 一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL bBroadcast=TRUE;
setsockopt (s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
8. 在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可
以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的
作用,在阻塞的函数调用中作用不大)
BOOL bConditionalAccept=TRUE;
setsockopt (s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));
9 . 如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们
一般采取的措施是"从容关闭"shutdown(s,SD_BOTH),但是数据是肯定丢失了,如何设置让程序满足具体
应用的要求(即让没发完的数据发送出去后在关闭socket)?
struct linger {
u_short l_onoff;
u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)
// 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;
m_sLinger.l_linger=5;//(容许逗留的时间为5秒)
setsockopt (s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
getaddinfo函数以及addrinfo结构体
getaddinfo函数
IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个addrinfo的结构(列表)指针而不是一个地址清单。这些addrinfo结构随后可由套接口函数直接使用。
头文件
1 |
函数原型
1 | int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result ); |
hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
hints:该参数指向用户设定的 struct addrinfo 结构体,只能设定该结构体中 ai_family、ai_socktype、ai_protocol 和 ai_flags 四个域,其余部分必须设置为0或者NULL。如果设置全为0,等价于 ai_socktype = 0, ai_protocol = 0,ai_family = AF_UNSPEC, ai_flags = 0
调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。
返回值:0——成功,非0——出错
EAI_ADDRFAMILY
指定的主机上没有请求的address family对应的网络地址.
EAI_AGAIN
DNS(name server)返回临时性错误. 可以稍后重试.
EAI_BADFLAGS
hints.ai_flags 包含了无效的标志; 或者 hints.ai_flags 包含了 AI_CANONNAME 标志但是 name 是 NULL.
EAI_FAIL
DNS(name server)返回永久性错误
EAI_FAMILY
不支持的 address family(hints.ai_family).
EAI_MEMORY
内存耗尽.
EAI_NODATA
指定的网络主机存在,但是其未定义任何网络地址.
EAI_NONAME
nodename 或者 servname 未知;或者两者都设置为NULL;或者设置了 AI_NUMERICSERV 标志但是 servname 不是一个数字化的端口名字符串。
EAI_SERVICE
请求的socket类型不支持请求的服务类型.例如服务类型是 “shell” (基于流的socket服务),但是 hints.ai_protocol 是 IPPROTO_UDP 或者hints.ai_socktype 是 SOCK_DGRAM;或者 servname 不是NULL 但是 hints.ai_socktype 是 SOCK_RAW (原始套接字不支持服务的概念).
EAI_SOCKTYPE
不支持请求的socket类型. 例如, hints.ai_socktype 和 hints.ai_protocol 冲突 (例如分别是SOCK_DGRAM、IPPROTO_TCP).
EAI_SYSTEM
系统调用错误,检查 errno.
addrinfo结构体
1 |
|
与socket接口的交互案例
1 | /****************************************************************************** |