详解socket通信

什么是socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议,对程序员来说,只要用好socket相关的函数,就可以完成数据通信。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。可以认为socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

socket是如何通信的

socket通信过程
服务器端先初始化Socket
初始化socket
然后与端口绑定(bind)
与端口绑定
对端口进行监听(listen)
对端口监听

调用accept阻塞,等待客户端连接。
accept阻塞
在这时如果有个客户端初始化一个Socket
客户端socket

然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
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只用于客户端。例:
geihostbyname

  • 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返回,至此三次握手完毕,连接建立。

由于水平有限,本博客难免有不足,恳请各位大佬不吝赐教!
如果文章有错别字,或者内容有错误,或其他的建议和意见,请您留言指正,非常感谢!!!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值