有关Socket编程的基础和问题汇总:
1. TCP Socket、UDP Socket基本操作
一个socket套接字有五个关键属性,包括协议,本地地址/端口,目的地址/端口。
服务端,可以通过socket函数指定协议,bind函数绑定本地地址/端口。
客户端,可以通过socket函数指定协议。
另外,无连接套接字服务端还可以通过connect函数,让内核记录下对方的IP地址和端口,并为UDP服务器绑定了一个临时端口和IP。(这样,UDP服务器只接收目的地址为其绑定的IP和端口,且源地址为其指定对方的IP和端口的数据报)
面向连接套接字,通过connect函数指定对方地址/端口,与此同时,内核会根据外出接口为该socket绑定一个IP和端口。
无连接套接字,通过sendto函数指定对方地址/端口,与此同时,内核会为该socket指定一个未被占用的端口;
也可以通过bind函数绑定地址/端口,然后,调用send函数指定对方地址/端口发送;
还可以调用connect函数,指定对方地址/端口,同时,绑定本地的地址/端口。
面向连接套接字:
Server Client
socket() socket()
bind()
listen()
accept() connect()
recv() send() --- 收、发均有缓冲区
send() recv()
close() close()
无连接套接字:
Server Client
socket() socket()
bind()
recvfrom() sendto() --- 只有接收缓冲区,没有发送缓冲区
sendto() recvfrom()
close() close()
2. 无连接Client端的 bind 操作
一般来说,UDP客户端在建立了套接字后会直接用sendto函数发送数据,在sendto函数的参数里
会指明目的地址/端口。同时,内核还会为该套接字选择一个独立的UDP端口(非知名端口),并将该
端口置为已绑定状态。
如果一个UDP客户端在建立了套接字后,用bind函数指明了本地地址/端口,这样可以强制UDP使
用指定的端口发送数据。也就是说,若不调用bind,则客户端在向外发包时,会由系统决定使用的接口
的源端口,而调用bind则可以指定相应的参数。
然而,面向连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息,
无须调用bind()。因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接
并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候
打开端口。(当然也有特殊情况,linux系统中rlogin命令应当调用bind函数绑定一个未用的保留端口
号,还有当客户端需要用指定的网络设备接口和端口号进行通信等)。
总结,可以使用bind函数的对象有:面向连接的服务端、无连接的服务端和无连接的客户端。
另外,在使用bind函数时,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端
口来使用。
3. 无连接socket的 connect 操作
UDP是一个无连接的协议,connect函数似乎对UDP是没有意义的,但实际上,UDP客户端也是允
许使用connect函数的。
如果一个UDP客户端在建立了套接字后,首先用connect函数指明了目的地址/端口,则可以用send
函数发送数据,因为此时send函数已经知道对方地址/端口,用getsockname也可以得到这个信息。
UDP客户端调用connect的另外一个作用是能够捕获错误。
由于UDP是无连接的,connect在调用时其实没有向外发包,只是在协议栈中记录了该状态。之后如
果发生网络异常,比如对端不可达,客户端在往对端写数据后,本机会收到一个ICMP回应,则回来的
ICMP不可达的响应能够被协议栈处理,通知客户端进程;当客户端再次对该fd进行操作时,比如读数据
时,read等调用会返回一个错误。而不调用connect时,对于返回的ICMP响应,协议栈不知道该传递给
上层的哪个应用,所以客户端进程中捕获不到相应的错误。
另外,UDP服务器也可以使用connect,如上面所述,connect可以用来指明目的地址/端口,这将
导致服务器只接受特定一个主机的请求。
4. 面向连接的Server端的 accept 操作
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个
套接字是连接套接字(一个具体五元组)。
此时我们需要区分两种套接字:
监听套接字 --- 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,
是服务器开始调socket()函数生成的,称为监听socket描述字(监听套接字)
连接套接字 --- 一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的
是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd
一样的端口号。
新socket与sockaddr的关系:
accept创建的新socket首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;
其次,因为它响应于client端connect函数的请求,所以,它还包含了clinet端sockaddr的信息。
而stream流形式的TCP协议实际上是建立起一个“可来可去”的通道。用于listen的通道,远程机的
目标地址是不确定的;但是 newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是
我们真正用于TCP“通讯”的socket。
5. 面向连接的三次握手(建立连接)和四次握手(断开连接)
当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;
服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,
这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K
进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
关于四次握手,需要注意的细节:
1)默认情况下(不改变socket选项),当你调用close时,如果发送缓冲中还有数据,TCP会继续把数据
发送完。
2)发送了FIN只是表示这端不能继续发送数据(应用层不能再调用send发送),但是还可以接收数据。
6. Socket接口的阻塞与非阻塞(I/O层)
Socket套接字在阻塞和非阻塞两种模式下执行I/O操作。在阻塞模式下,在I/O操作完成前,执行的操作
函数会一直等待而不会立即返回,同时也意味着该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,
套接字函数会立即返回,而不管I/O操作是否完成,同时也意味着该函数所在的线程会继续运行而不阻塞在
这里。
并不是Socket整个流程,或者说所有相关的函数都会有阻塞模式。比如,bind()、listen()函数是
不会阻塞的,即便当前socket处于阻塞模式下;而recv()/recvfrom()、send()/sendto()、
accept()、connect()函数是会阻塞的。除了上述socket自身拥有的处理函数之外,还有一些与socket
相关的I/O处理函数需要注意,如用来实现I/O复用的select()函数。select函数也会是进程阻塞,但是和
上述函数不同的是,它可以同时阻塞多个I/O操作。而且可以同时对多个读操作、多个写操作的I/O函数进行
检测,直到有数据可读或可写时,才返回告知调用者。但是,select函数是有超时设置的,也就是说,不会
无限阻塞下去。
使用socket()函数创建套接字时,其默认都是阻塞模式的。创建后,可以通过fcntl()将其修改为非
阻塞模式(linux编程)。
阻塞模式和非阻塞模式的应用:
1)阻塞模式,常见的通信模型为多线程模型,服务端accept之后,对每个socket创建一个线程去recv。
逻辑上简单,适用于并发量小(客户端数目少),连续传输大数据量的情况下,比如文件服务器。还有就是
在客户端recv服务器消息的时候也经常用,因为客户端就一个socket,用阻塞模式不影响效率,而且编程逻
辑上要简单得多。
2) 非阻塞模式,常见的通信模型为select模型和IOCP模型。适用于高并发,数据量小的情况,比如聊天
室。客户端多的情况下,服务端如果采用阻塞模式,需要开很多线程,影响效率。另外,客户端一般不采用
非阻塞模式。
7. 字节序
由于历史的原因,业界存在两种主机字节序标准:BigEndian(大头、大端)和LittleEndian(小头、小端)
典型的使用小端存储的CPU有:Intel x86和ARM
典型的使用大端存储CPU有:Power PC
有些CPU可以通过寄存器设置支持不同的字节序,例如MIPS
对大部分网络传输协议而言,网络字节序一般是指大端传输。
Big-Endian和Little-Endian。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
c) 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于 TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。比如,以太网头部中2字节的“以太网帧类型”,表示后面数据的类型。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是0x08,0x06。
相同字节序的平台在进行网络通信时可以不进行字节序转换,但是跨平台进行网络数据通信时必须进行字节序转换。
UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,所以如果发送端主机是小端,应该把多字节数据转换成网路字节序在内存中存放,这样在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个转换成网路字节序后对应的高位字节。
而接收的时候接收端会把接收到的第一个字节放到接收端主机的低地址,而第一个发送过来的是高位字节,所以如果接收端主机是小端模式,应该进行网络字节序到小端的转换。
为何字符串/数组无需考虑字节序问题呢?
因为字符串和整数存储的机制不同,无论是在大端序主机还是在小端序主机中,字符串都是根据索引顺序从内存的低地址到高地址存储的。
网络字节序和主机字节序的转换函数:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
8. 字节对齐
重要规则(结合#pragma pack):
1. 复杂类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同;
2. 每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度;规则就是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐;
3. 结构、联合或者类的数据成员,第一个放在偏移为0的地方;以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度两个中比较小的那个进行;也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果;
4. 复杂类型(如结构)整体的对齐<注意是“整体”>是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行;这样在成员是复杂类型时,可以最小化长度;
5. 结构整体长度的计算必须取所用过的所有对齐参数的整数倍,不够补空字节;也就是取所用过的所有对齐参数中最大的那个值的整数倍,因为对齐参数都是2的n次方;这样在处理数组时可以保证每一项都边界对齐
详细规则可参考:
http://blog.youkuaiyun.com/qq2012qiao/article/details/39204673 点击打开链接
http://baike.baidu.com/view/2317161.htm?fr=aladdin 点击打开链接
9. 阻塞模式和epoll ET模式下accept存在的问题
阻塞模式下accept存在的问题:
考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。
解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。
epoll ET模式下accept存在的问题:
考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环调用accept,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
截自:http://www.ccvita.com/515.html 点击打开链接