socket是“插座”的意思,两个进程之间通过socket来进行通信可以用手机来比喻,一般都是客户端向服务器发出访问请求,则客户端类比为拨电话的人,而服务器类比为接电话的人。两个用户要对话首先双方都要有一部手机,相当于双方用socket()函数创建一个socket套接字一样,然后这部手机要有一个电话号它才能有利用价值,也就是将套接字与通信地址进行挂钩。对于客户端,也就是准备拨电话的人,他必须要知道他想给谁打也就是对方的电话号是多少,通过connect()函数将它想访问的通信地址与该套接字进行连接。而接电话的人他要给这部电话一个电话号,其他人才能打过来,所以利用bind()给这部电话绑定一个电话号。这样就能看出,如果双方想要成功的建立连接,双方的通信地址必须是一样的,就像我要给你打电话,必须拨打的是你的手机号一样。
那这个手机就有不同了,有的是国际号,有的是家里的小号,不同的号对电话的要求不太一样,拿着小灵通就打不到国外了……而再然后,有的打电话就必须对方接到再进行通话,有的可以留言,对方什么时候有空再听。前者代表的是协议族,有本地通信和ipv4 ipv6 网络通信,后者表示连接类型,必须连接是TCP通信类型,可以留言的是UDP通信类型,通过选择参数选择合适的电话机。
对这个电话号码也是有要求的,家里的小号是短号,国外的可能就多好几位,对于本地通信和网络通信有通用的通信地址,不需要我们自己定义,分别是struct sockaddr_un和struct sockaddr_in,但是在bind和connect过程中为了方便,我们都把他们强制转换为通用的一个地址格式struct sockaddr。有一点要注意就是在网络通信地址里的端口号,由于网络字节和主机字节很多时候是不一样的,网络字节是大端网络,而主机字节很多情况下是小端网络,也就是假设端口号是1234,那么在计算机里存储是地址由低到高:0x34 0x12,但在网络上存储时地址由低到高:0x12 0x34,这个时候利用htons()函数转变字节顺序。
一:本地通信和UDP通信
都不是面向连接的,过程大致为
服务器:
(1)创建socket,使用socket函数
(2)准备通信地址,使用结构体类型
(3)绑定socket和通信地址,使用bind函数
(4)进行通信,使用read/write函数
(5)关闭socket,使用close函数
客户端:
(1)创建socket,使用socket函数
(2)准备通信地址,服务器的地址
(3)链接socket和通信地址,使用connect函数
(4)进行通信,使用read/write函数
(5)关闭socket,使用close函数
二:TCP通信
是面向连接的,所以接电话的人先给自己手机绑定一个手机号以后,就一直等着电话响,这就是listen(),等到发现电话响了,马上把电话接起来,这就是accept(),然后双方就可以进行通话了。注意这里accept()函数会返回一个文件描述符,服务器是通过返回的这个描述符来通信而不是像UDP一样用socket的套接字来通信。
过程如下:
服务器:
(1)创建socket,使用socket函数
(2)准备通信地址,使用结构体类型
(3)绑定socket和通信地址,使用bind函数
(4)监听,使用listen函数
(5)响应客户端的连接请求,使用accept函数
(6)进行通信,使用read/write函数
(7)关闭socket,使用close函数
客户端:
(1)创建socket,使用socket函数
(2)准备通信地址,使用结构体类型
(3)连接socket和通信地址,使用connect函数
(4)进行通信,使用read/write函数
(5)关闭socket,使用close函数
一:bind出现Address already in use:
1:本地通信:unixdomain socket 与网络socket编程最大的不同在于地址格式不同,用结构体socketaddr_un表示,网络地址是由ip加端口号决定,而domain socket的地址是一个socket类型的文件在文件系统的路径,该文件由bind()函数创建并绑定,如果bind时该文件已经存在,则绑定失败。因此每次把创建的socket文件删除或者bind一个新的socket文件。
2:网络通信:程序第一次可以正确运行并且结束,但第二次开始就会出现错误bind:Address already in use,只能用ctrl+c强制结束,这个问题是由TCP 套接字状态TIME_WAIT 引起,在套接字通过close()正常关闭后,会保留2到4分钟,该套接字才会删除,同时与该套接字绑定的端口和本地地址才可以被重新绑定。所以如果我们运行过一次程序,用close(sockfd)删除这个套接字之后,其实要等到几分钟之后,才会真正删除,这段时间内的端口和本地地址是仍然与该套接字绑定的,如果你立即再执行一遍程序,便会提示:这个地址正在使用中。
有一个绕过TIME_WAIT的方法就是,给套接字设置一下,让它可以绑定一个复用的端口就可以了。具体是利用setsockopt函数。
int setsockopt(SOCKET s,int level,intoptname,const char* optval,int optlen)
SOCKET(套接字): 指向一个打开的套接口描述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其他结构类型:linger{},timeval{ }
optlen(选项长度) :optval 的大小
返回值:标志打开或关闭某个特征的二进制选项
因此可以加这么一句:
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,&reuse, sizeof(reuse));
(SO_REUSERADDR 允许重用本地地址和端口,充许绑定已被使用的地址(或端口号))
二:bind: Cannot assign requested address
首先作为服务器bind时候,必须绑定自己的ip地址,不能乱写成别的ip地址,另外在tcp中端口号不能被占用,可以加上上一题里那两句允许复用的程序,就可以绑定占用的端口。程序测试发现udp里可以绑定正在使用中的端口(留疑问)。
然后服务器和客户端要通信,两端通信地址必须完全一样,ip和端口都要一致。
三:connect: Connection refused
客户端要和服务器连接时,服务器必须是已经打开的,所以要先开服务器再开客户端。
四:read:Bad address
大部分是由于读写内存地址错误的问题
五:read: Connection reset by peer
客户端要读数据时,此时服务器已经关闭了
六:服务器这端在read()数据时,会接收两次数据
如果write()端的发送数据大小大于read()端的接收数据大小,数据会分成两批进行接收。因此要保持两端的数据大小一致或者read端大于write端