TCP通信
一个程序使用套接字需要执行4个步骤。
--分配套接口和初始化
--连接
--发送或接收数据
--关闭套接字
涉及到的调用包括socket、bind、listen、connect(阻塞线程)、accept(阻塞线程)、recv(阻塞线程)、send(阻塞线程)。
分配套接口和初始化
--我们需要做的第一件工作就是分配套接口。
--套接口可以看作是文件描述符
--不论server端,还是client端,第一步都是一样的
每个套接口都是一个通信的通道
两个进程通过套接口建立连接后就可以发送和接收数据了。
socket()
int socket(int domain.int tyoe,int protocol);
系统调用socket带有以下参数
--int domain
--int tyoe
--int protocol(这个值一般都取0)
--成功返回套接字描述符,失败返回-1,并设置errno
socket参数
domain说明
AF_UNIX UNIX内部使用
AF_INET TCP/IP协议
AF_ISO 国际标准组织协议
AF_NS Xerox网络协议
type说明
SOCK_STREAM 使用TCP可靠连接
SOCK_DGRAM 使用UDP不可靠连接
在写网络程序的时候,建立TCP socket:sock = socket(PF_INET, SOCK_STREAM, 0);然后再绑定本地地址或连接远程地址时需要初始化sockaddr_in结构,
其中指定address family时一般设置问AF_INET,即使用IP。
相关的头文件定义:
AF = Address Family
PF = Protocol Family
AF_INET = PF_INET
所以在windows中,AF_INET与PF_INET完全一样,而在Unix/Linux系统中,在不同的版本中这两者有微小差别。
对于BSD,是AF,对于POSIX是PF。理论上,建立socket时是指定协议,应该用PF_XXXX,设置地址时应该用AF_XXXX。
当然AF_INET和PF_INET的值是相同的,混用也不会有太大的问题。
bind()函数
int bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen);
bind将进程和一个套接口联系起来,bind通常用于服务器进程为接入客户连接建立一个套接口(简单而言,就是把程序和一个IP地址端口号绑定在一起)。
参数sockfd是函数socket调用返回的套接口。
参数my_addr是结构sockaddr的地址(用来描述IP地址的一个结构)。
参数addrlen设置了my_addr能容纳的最大字节数。
成功返回0,失败返回-1,并设置errno。
一个端口号只能绑定一个程序,1对1关系
socklen_t本质上是unsigned int,并不是int,在windows操作系统下才是int。
INADDR_ANY是ANY,是绑定地址0.0.0.0上的监听, 能收到任意一块网卡的连接;
INADDR_LOOPBACK, 也就是绑定地址LOOPBAC, 往往是127.0.0.1, 只能收到127.0.0.1上面的连接请求
对于客户端程序,下一步是要与之通信的服务器建立连接。
--客户端只需使用connect即可
对于服务端程序,就是要建立自己的套接口等待来自客户端的连接。
--服务器需要调用listen和accept两个函数。
listen()函数
int listen(int sockfd,int backlog);
创建了套接口并且使用bind将它和一个进程关联起来以后,服务端就需要调用listen来监听指定端口的客户端连接。
参数sockfd是调用socket返回的套接口描述符
参数backlog设置接入队列的大小,通常把这个值设置的足够大就可以了。
参数backlog一般用来设置服务端可以并发的接收客户端的最大连接数目
listen也是从TCP缓存中读取连接,并非来一个接收一个。
成功返回0,失败返回-1,并设置errno
listen是将连接直接放到缓存区里,等待accept函数来接收
accept()函数
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
当有客户端连接到服务端,他们会排入队列,知道服务端准备好处理他们位置为止,accept会返回一个新的套接口,同时原来的套接口继续listen指定端口号
参数sockfd是调用socket返回的套接口描述符
参数addr指向结构sockaddr地址,表示客户端的IP地址。
参数addrlen设置了addr能容纳的最大字节数。
成功返回新的套接字,失败返回-1,并设置errno
connect()函数
int connect(int sockfd,const struct sockaddr * serv_addr,socklen_t addrlen);
客户端调用connect与服务端进行连接。
参数sockfd是调用socket返回的套接口描述符。
参数addr指向结构sockaddr地址。
参数addrlen设置了addr能容纳的最大字节数。
成功返回0,失败返回-1,并设置errno。
connect()函数也是阻塞的,它必须完成三次握手机制才能返回。
客户端和服务端建立了连接就可以在客户端和服务端之间传输数据了,需要两个系统调用。
--send 发送数据。
--recv 接收数据。
一个套接口既可以发送数据,也可以接收数据,网络是一个双向管道。
send()函数
ssize_t send(int s,const void *buf,size_t len,int flags);
send函数用来发送数据。
参数s是已经建立连接的套接口。
参数buf是发送数据内存buffer地址指针。
参数len指明buffer的大小,单位字节。
参数flags一般填0.
成功返回发送的字节数,失败返回-1,并设置errno。
send()函数注意点
1.send返回值理解
send在阻塞场景下,返回值要么是指定长度(发送成功),要么是-1,发送失败,但是在非阻塞场景下,
返回值可能小于指定长度,这是因为发送数据超过发送缓冲区(窗口),所以只能发送缓冲区大小的数据,剩下的数据无法发送
2.errno=11的理解
send在非阻塞场景可能返回-1,并且更新errno为11,11表示资源临时不可用,当发送缓冲区满了,
而程序不断在调用send(0函数发送数据就会出现这个错误,此时收到返回值为-1,并且errno=11时,需要停止发送数据,等待套接字下次可写的时候再发送数据。
recv()函数
ssize_t recv(int s,void *buf,size_t len,int flags);
recv函数用来接收数据。
参数s是已经建立连接的套接口。
参数buf是接收数据内存buffer地址指针。
参数len指明buffer的大小,单位字节。
参数flags一般填0.
成功返回接收到的字节数,失败返回-1,并且设置errno,如果对端套接字已经关闭,返回0.
recv函数只是从TCP缓存中读数据(此时数据已经在自己的电脑上了),不是直接从网络中读数据,什么时候TCP缓存区满了,另一边的send函数才会停止发送数据。
recv()函数会阻塞线程,直到收到消息或者客户端关闭。
ssize_t recv(int s, void *buf, size_t len, int flags);
参数flags的值为 MSG_PEEK 介绍
当flags的值设置为0,此时recv函数将读取 socket 缓冲区中的数据到 buf 中,并且会移除 socket 缓冲区中已读取的数据
当flags的值设置为MSG_PEEK,此时recv函数将读取 socket 缓冲区中的数据到 buf 中,但是不会移除 socket 缓冲区中已读取的数据(偷看一下缓冲区中数据)
最后当你用完套接口以后,就该释放套接口所占用的资源了,通过close做到这一点
当试图向一个已经关闭的套接口写或者读数据就会出错。
setsockopt()函数
int setsockopt(int s,int level,int optname,const void *optval,socklen optlen);
setsockopt函数设置套接口
函数成功返回0,失败返回-1,并设置errno
常见用法为:
int on=1;
setsockopt(st,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
由于TCP套接字状态TIME_WAIT引起该套接字关闭后约保留2到4分钟。在此期间bind绑定该端口失败。
SO_REUSEADDR指示系统地址可重用。
当服务器在listen()函数后直接退出,如果这时候有一个客户端来连接,这个连接会被放在缓存中,如果这时候启动了一个新的程序绑定这个IP地址和端口号,
那么这新的程序就会接收到上一个客户端的请求,这是有问题,因为上一个客户端请求访问的是原始程序。但是如果我们就一直一个程序绑定这个IP地址,就用不着这个TIME_WAIT信号了,
我们服务器上就一个server绑定这个IP地址和端口号,所以用不着这个机制,因此调用setsockopt()函数。
IP地址的结构
ip地址在内存中用int表示,int在内存中占有4个字节的空间
第一个字节:192
第二个字节:168
第三个字节:1
第四个字节:2
send()、recv()函数原理解析
通过上一章网络编程一中的主机之间的通讯图可以得知,两主机之间通信,主机A先将消息打包成TCP包,TCP包再打包成IP包,然后形成以太网包发送,
另一台主机的接收顺序恰好相反,先接受以太网包,再接收IP包,最后接收TCP包,通过程序中测试,recv()函数一次能够接受的数据(red hat中最大能接收64K数据)要
比send()函数发送的数据(red hat中最大发送的数据超过128K),send()函数是将数据打包成TCP包再发送,而接收数据的时候是接收的IP包,再将IP包还原成TCP包,
这说明TCP包的容量实际上会大于或者等于IP包,事实上计算机在发送数据时,如果TCP包过大,会把TCP拆解成多个IP包,并将这些IP包存储在网卡的缓存区里(如果send
发送数据超过缓冲区,那么sned()所在的线程就会被挂起),recv在网卡上也有缓存区,网络发送过来的数据会先存放在缓存区中,直到缓存区被填满,此时发送方就会停
止发送,但是此时不意味这send函数所在的线程就会被挂起,send函数还可以往自己的缓存区中发数据,直到缓存区填满。
在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),
这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,
这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。
对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
另外,如果出现EINTR即errno为4,错误描述Interrupted system call,该错误是由于操作被信号打算,操作也应该继续。
最后,如果recv的返回值为0,那表明连接已经断开,我们的接收操作也应该结束。