自以为对于UDP,TCP的理解还算深刻,但是具体到使用的时候还是经常犯糊涂,现在再梳理梳理。
在网络编程中,用到的一些函数有:socket,bind,listen,accept,connect,send,sendto,recv,recvfrom等。
socket()用于创建一个套接字。
bind()将套接字和本地地址结构绑定。
listen()将套接字设置为监听状态。
connect()用于向另一端发送连接请求。
accept()用于接受另一端的连接请求。
send(),sendto()用于向对端发送数据。
recv(),recvfrom()用于接收对端发来的数据。
在网络上通讯的两个程序中,一般都是一方扮演服务者的角色,一方扮演客户的角色。服务器一般首先通过socket()函数创建一个套接字,然后通过bind()函数将该套接字和本地地址(协议,地址,端口等信息的集合)进行绑定,绑定的目的在于通知操作系统,应用程序请求关注进入本地主机某个网络接口上的某个端口的数据包:若有满足这个条件的数据包,请交给我处理。例如本地主机有两个网卡AB,IP地址分别为192.168.1.2/255.255.0.0和192.168.2.2/255.255.0.0,某应用程序需要接收来自A网卡,目的端口为8888的数据包,那么应用程序可以将IP 192.168.1.2,端口8888的地址信息和套接字绑定。当网卡A接收到一个目的端口为8888的数据包时,它就会交给应用程序来处理。
上述操作仅仅是将套接字和地址信息绑定,内核并没有执行真正的接收数据包并提交的操作,这个操作要等到用户调用了listen()函数时才发生。listen()将使内核执行真正的”监听操作”, 接下来当有一个来自网络的连接请求时,内核将该连接请求交给应用程序,应用程序可以调用accept()函数来接受这个连接请求。
accept()原型如下:
该函数在没有连接请求时默认进入阻塞状态,直到有连接请求。该函数调用返回后,将会有一个来自对端的地址信息被保存在addr中,并且返回一个套接字,在以后的操作中,应用程序就可以通过这个套接字和地址信息与对端进行通讯,而原来的套接字则可以继续进行监听操作。通常socket(),bind(),listen(),accept()是服务端程序都需要进行操作,而作为客户端,则执行socket(),connect()之后,就可以通讯了。
socket()同样用于产生一个套接字,connect()函数是比较神奇的,原型如下:
对于TCP它将向参数中指定的远程服务器发出连接请求,若对端服务器调用了accept()接受了连接请求(完成TCP3次握手)之后,双发就可以调用send(),sendto(),recv(),recvfrom()来进行数据的收发了。
recv(),recvfrom()与send() ,sendto()的区别在于,sendto(),recvfrom()可以指定地址,即,sendto()每次发送时可以指定一个目的地址,recvfrom()可以获得来自对端的地址信息。
这也引发一个问题,就是TCP和UDP编程时,使用这些函数的差别。
当对方地址已知,并且和套接字建立了联系之后,就可以直接调用send()函数,当对方地址已知但是没有和套接字绑定时,可以sendto()的参数来指定对方地址。
recv()和recvfrom()区别在于,recv()只接收特定地址的数据包,因为在这之前,地址和套接字已经通过某些方式关联在一起了,如accept(),connect()函数,accept已经得到了对端的地址,并且和套接字绑定在一起,connect()把目的地址和本地的套接字绑定在一起,双方的通讯可以建立在已经形成的绑定关系上,所以发送时无需再指定地址,接收时也仅仅接收来自与该套接字绑定地址相同的数据包。
综上所述:
bind()就是用来把套接字和地址信息绑定的,一般服务器使用来设置原地址和端口,以表明在哪一个端口提供服务,该端口是应该要告知客户端的,如果不设置,那么就又内核自动分配,此时服务器也可以进入监听或接收状态,这样客户端是无法知道这个端口号,从而无法连接或发送数据到这个端口。客户端也可以用来设置自己的原地址和端口(一般都不使用,仅由内核自动提供动态端口,因为服务器不关心客户端的端口是多少)。
connect()是客户端用来设置目的地址,TCP和UDP使用的效果有所不一样,TCP设置目的地址之后就开始发送连接请求,进行3次握手,UDP仅仅是设置目的地址,因为UDP是无连接的,无需三次握手。服务器无需使用。 UDP调用了connect()之后,也可以使用recv(),send(), 或者recvfrom(),sendto()相应地址参数可以设置为NULL。