socket

来源:互联网 发布:数据库的三级模式好处 编辑:程序博客网 时间:2024/04/29 09:44

两个进程通讯最基本的前提是能唯一的标识一个进程,在本地进程通讯中可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。

能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。

socket通信过程

打开—读/写—关闭


1、服务器根据地址类型、socket类型、协议创建socket

地址类型:

AF_INETAF_INET6AF_LOCALAF_ROUTE其中AF_INET代表使用ipv4地址

socket类型:

常见的socket有3种类型如下。 

(1)流式socket(SOCK_STREAM ) 

 流式套接字提供可靠的、面向连接的通信流;它使用TCP 协议,从而保证了数据传输的正确性和顺序性。 

(2)数据报socket(SOCK_DGRAM ) 

数据报套接字定义了一种无连接的服 ,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。 

(3)原始socket(SOCK_RAW)

原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。

2、服务器绑定ip地址和端口号

3、服务器监听端口号请求,随时准备接受客户端发来的连接,这是服务器socket没有打开

4、客户端创建socket

5、客户端打开socket,根据IP地址和端口号试图连接服务器socket

6、服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求

7、客户端连接成功,向服务器发送连接状态信息

8、服务器accept方法返回,连接成功

9、客户端向socket写入信息

10、服务器读取信息

11、客户端关闭

12、服务器端关闭

TCP、IP三次握手


第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

定睛一看,服务器socket与客户端socket建立连接的部分其实就是大名鼎鼎的三次握手


<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; line-height: 21.6000003814697px; font-family: 'Courier New' !important;"><span style="color: rgb(0, 0, 255); line-height: 1.5 !important;"></span><pre style="margin-top: 0px; margin-bottom: 0px;font-size:10px; text-indent: 28px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; line-height: 21.6000003814697px; font-family: 'Courier New' !important;"><span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">int</span> socket(<span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">int</span> domain, <span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">int</span> type, <span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">int</span> protocol);

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);

struct sockaddr与struct sockaddr_in ,struct sockaddr_un的区别和联系

struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。一般情况下,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中。

关于SO_REUSEADDR的使用说明~

1.   可以对一个端口进行多次绑定,一般这个是不支持使用的;   
  2.   对于监听套接字,比较特殊。如果你定义了SO_REUSEADDR,并且让两个套接字在同一个端口上进行接听,那么对于由谁来ACCEPT,就会出现歧义。如果你定义个SO_REUSEADDR,只定义一个套接字在一个端口上进行监听,如果服务器出现意外而导致没有将这个端口释放,那么服务器重新启动后,你还可以用这个端口,因为你已经规定可以重用了,如果你没定义的话,你就会得到提示,ADDR已在使用中。 

总结:防止服务器在发生意外时,端口未被释放~可以重新使用~


getpeername:返回远程协议地址

应用层

 

int getsockname(int s, struct sockaddr *name, socklen_t *namelen);

Get the current name for the specified socket.

获取本地套接口的名字,包括它的IP和端口。

 

int getpeername(int s, struct sockaddr *name, socklen_t *namelen);

Get the name of connected peer socket.

获取远程套接口的名字,包括它的IP和端口。

 

getsockname()在指定的套接口绑定地址和端口后才能调用,即服务器在bind()后可调用,

客户端在bind()或connect()之后可调用。getpeername()在连接建立之后才可调用。


补充:getsockname和getpeername调度时机很重要,如果调用时机不对,则无法正确获得地址和端口。
TCP
对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。
对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getpeername的时机。
UDP
UDP分为链接和没有链接2种(这个到UDP与connect可以找到相关内容)
没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后
已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。

EINTR错误


Linux下tcp连接断开的时候调用close()函数,有优雅断开和强制断开两种方式。

那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。

linger结构体数据结构如下: 

<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; line-height: 18px; font-family: 'Courier New' !important;"><span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">#include <arpa/inet.h></span><span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">struct</span> linger {<span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">  int</span> l_onoff;<span style="color: rgb(0, 0, 255); line-height: 1.5 !important;">  int</span> l_linger;};

三种断开方式:

1. l_onoff = 0; l_linger忽略

close()立刻返回,底层会将未发送完的数据发送完成后再释放资源,即优雅退出。

 

2. l_onoff != 0; l_linger = 0;

close()立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,即强制退出。

 

3. l_onoff != 0; l_linger > 0;

close()不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,close()会返回正确,socket描述符优雅性退出。否则,close()会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则close()会直接返回值。

具体用法:
struct linger ling = {0, 0};setsockopt(socketfd, SOL_SOCKET, SO_LINGER, (void*)&ling, sizeof(ling));






0 0