什么是socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议,对程序员来说,只要用好socket相关的函数,就可以完成数据通信。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。可以认为socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
socket是如何通信的
服务器端先初始化Socket
然后与端口绑定(bind)
对端口进行监听(listen)
调用accept阻塞,等待客户端连接。
在这时如果有个客户端初始化一个Socket
然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
相关函数
1、socket函数
int socket(int domain, int type, int protocol);
使用socket函数向系统申请一个socket资源,socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字(一般从3开始,GDB调试是从7开始),而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
- domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。其实一般都填写AF_INET。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等,流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。一般我们填写SOCK_STREAM。
- protocol:就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,一般我们填个0就可以了。
函数的错误信息可以用perror函数返回
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
// 先打印出socke,后面再加上错误原因字符串
perror("socket"); return -1;
}
注意:除非系统资料耗尽,socket函数一般不会返回失败。
2、bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。
- addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址,不同的协议使用不同的结构体来表示,如常用的ipv4是
// bind函数对应的结构体
struct sockaddr {
unsigned short sa_family; // 地址类型,AF_xxxx
char sa_data[14]; //14字节的端口和地址
}
//ipv4对应节结构体,用这个是为了方便书写
struct sockaddr_in {
sa_family_t sin_family; // 地址类型
in_port_t sin_port; // 端口
struct in_addr sin_addr; // 地址
unsigned char sin_zero[8]; // 为了与struct sockaddr保持一样的长度
};
struct in_addr {
uint32_t s_addr;
};
//其他的不常用就不做介绍了
- addrlen:对应的是地址的长度。
注意:如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。1024以内的端口一般为系统端口,最大的端口是65535
3、listen()函数
int listen(int sockfd, int backlog);
listen函数把主动连接套接字变为被动连接的套接字,使得这个socket可以接受其它socket的连接请求,就是在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。
- backlog:为相应socket可以排队的最大连接个数。为数组的下标,设置为5可以有6个排队数量
4、connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
向服务器发起连接请求。客户端通过调用connect函数来建立与TCP服务器的连接。
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。
- addr:服务器的socket地址
- addrlen:socket地址的长度
5、accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:服务端的socket描述字
- addr:存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可以填0。
- addrlen:协议地址的长度。
TCP服务器监听到客户端的连接请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
6、send函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
不论是客户端还是服务端,send函数用于把数据通过socket发送给对端。函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。如果send函数返回的错误(<=0),表示通信链路已不可用。
- sockfd:为已建立好连接对方的socket。
- buf:为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。
- len:需要发送的数据的长度
- flags:填0, 其他数值意义不大。
7、recv函数
不论是客户端还是服务端,recv函数用于接受对端发送过来的数据,如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1,错误信息errno被标记。如果socket被对端关闭,返回值为0。如果recv函数返回的错误(<=0),表示通信通道已不可用。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd:为已建立好连接对方的socket。
- buf:为需要接收的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。
- len:需要发送的数据的长度,不可以超过buf的大小,否则会造成内存溢出
- flags:填0, 其他数值意义不大。
8、gethostbyname函数
struct hostent *gethostbyname(const char *name);
把ip地址或域名转换为hostent 结构体表达的地址,gethostbyname只用于客户端。例:
- name:IP地址或者域名都可以
注意:gethostbyname只是把字符串的ip地址转换为结构体的ip地址,只要地址格式没错,一般不会返回错误。函数失败不会设置errno的值。
9、close()函数
int close(int fd);
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,因为socket是系统资源,操作系统打开的socket数量是有限的,在程序退出之前必须关闭已打开的socket(不仅仅只是在main()函数结束时,应该在每一个return之前),就像关闭文件指针一样,就像delete已分配的内存一样,极其重要。
设置服务端socket的SO_REUSEADDR属性
服务端程序端口释放后会处于TIME_WAIT状态,等待2分钟后才可以继续使用,设置这个属性后就可以直接使用了
// 在socket函数后增加以下代码
int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(listedfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);
什么是三次握手
tcp建立连接要进行“三次握手”,即交换三个分组,客户端给服务端发送一个信号,服务端向客户端给响应一个信号,客户端再给服务端发送一个确认信号。具体流程是客户端调用connect时,触发了连接请求,向服务器发送连接信号,这时connect进入阻塞状态;服务器监听到连接请求,调用accept函数接收请求并再向客户端给回应一个信号,这时服务端accept进入阻塞状态;客户端收到服务器的信号后connect返回,并对服务端发送再次发送信号进行确认;服务器收到信号后accept返回,至此三次握手完毕,连接建立。
由于水平有限,本博客难免有不足,恳请各位大佬不吝赐教!
如果文章有错别字,或者内容有错误,或其他的建议和意见,请您留言指正,非常感谢!!!