Linux编程(五)TCP/UDP

关于TCP/UDP的网络编程,在Linux下,是以文件为基础的。以下是UDP的框架图。

Linux编程(五)TCP/UDP

从图中可以看出,就是一次交互,不存在连上了,然后能一直交互,没得。客户端要发起一次请求,仅仅需要两个步骤(socket和sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。

大名鼎鼎的socket函数:

#include <sys/types.h>          
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain:用于设置网络通信的域其实就是信息协议的族

Name                                     Purpose                         

AF_UNIX, AF_LOCAL          Local communication              

AF_INET                           IPv4 Internet protocols          //用于IPV4

AF_INET6                         IPv6 Internet protocols          //用于IPV6

AF_IPX                             IPX - Novell protocols

AF_NETLINK                     Kernel user interface device     

AF_X25                            ITU-T X.25 / ISO-8208 protocol   

AF_AX25                          Amateur radio AX.25 protocol

AF_ATMPVC                      Access to raw ATM PVCs

AF_APPLETALK                 AppleTalk                        

AF_PACKET                      Low level packet interface       

AF_ALG                           Interface to kernel crypto API

 

对于该参数我们仅需熟记AF_INET和AF_INET6即可,其实很多,还有就是PF开头的,AF_前缀表示地址族(Address Family),而PF_前缀表示协议族(Protocol Family)。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_的值可以用来创建套接字,而AF_值用于套接字的地址结构。但实际上,支持多个地址族的协议族从来就没实现过,而头文件<sys/socket.h>中为一给定的协议定义的PF_值总是与此协议的AF_值相同。

参数type(只列出最重要的三个):

SOCK_STREAM         Provides sequenced, reliable, two-way, connection-based byte streams.   //用于TCP

SOCK_DGRAM          Supports datagrams (connectionless, unreliable messages ). //用于UDP

SOCK_RAW              Provides raw network protocol access.  //RAW类型,用于提供原始网络访问

参数protocol:置0即可

返回值:成功:非负的文件描述符

           失败:-1

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

第一个参数sockfd:本套接字了

第二个参数my_addr:需要绑定的IP和端口,是一个 结构体

第三个参数addrlen:my_addr的结构体的大小

返回值:成功:0

           失败:-1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

第一个参数sockfd:就是本套接字了

第二个参数buf:发送缓冲区

第三个参数len:发送缓冲区的大小,单位是字节

第四个参数flags:填0即可,不知道是啥

第五个参数dest_addr:表明要发给谁,它不是socket,它是一个结构体

第六个参数addrlen:表示第五个参数所指向内容的长度,就是结构体的长度

返回值:成功:返回发送成功的数据长度

           失败: -1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数sockfd:本套接字了

第二个参数buf:接收缓冲区

第三个参数len:接收缓冲区的大小,单位是字节

第四个参数flags:填0即可

第五个参数src_addr:我们可以从该参数获取到数据是谁发出的,这是一个结构体,用来回送的

第六个参数addrlen:表示第五个参数所指向内容的长度

返回值:成功:返回接收成功的数据长度

           失败: -1

#include <unistd.h>
int close(int fd);

close函数比较简单,只要填入socket产生的fd即可。

Linux编程(五)TCP/UDP

这个是TCP的,我发现如果想做好学习,最好办法就是重新写一份。。。

服务器端:
socket() --> bind() --> listen() --> accept() --> recv() --> close()
创建socket --> 绑定socket和端口号–> 监听端口号–> 接收来自客户端的连接请求–> 从socket中读取字符–> 关闭socket

bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。

-------------------------------------------------------------------
#include <sys/socket.h>  
 int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
 返回:0---成功   -1---失败 
 -------------------------------------------------------------------

listen函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。什么意思呢,就好像进入触发状态一样。

-------------------------------------------------------------------
#include <sys/socket.h>
 int listen(int sockfd,int link_n);   
 返回:0---成功   -1---失败
 -------------------------------------------------------------------

  第一个参数是socket函数返回的套接口描述字;第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。或者阻塞状态吧,有疑问,就是说它现在管理着的连接客户端吧,不用太清楚。

accept函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。白话讲,就是从来到的消息包队列中,抽出一个来,进行处理,如果消息队列中什么都没有

-------------------------------------------------------------------
#include <sys/socket.h>         
 int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);  
  回:非负描述字---成功   -1---失败
 -------------------------------------------------------------------

第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字(就是客户端的,接下来有用);如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。

write和read函数:当服务器和客户端的连接建立起来后,就可以进行数据传输了,服务器和客户端用各自的套接字描述符进行读/写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读/写函数write()和read()进行接收和发送操作。从这里可以看到,sockfd换做文件的id,同样不差,说明了socket基于文件系统了。

(1)write()函数用于数据的发送。

-------------------------------------------------------------------
#include <unistd.h>         
 int write(int sockfd, char *buf, int len); 
  回:非负---成功   -1---失败
 -------------------------------------------------------------------

参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符;参数buf是指向一个用于发送信息的数据缓冲区;len指明传送数据缓冲区的大小。

 

(2)read()函数用于数据的接收。

-------------------------------------------------------------------
#include <unistd.h>         
 int read(int sockfd, char *buf, intlen);  
  回:非负---成功   -1---失败
 -------------------------------------------------------------------

参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符;参数buf是指向一个用于接收信息的数据缓冲区;len指明接收数据缓冲区的大小。

send和recv函数:TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。

(1)send()函数用于数据的发送。

-------------------------------------------------------------------
#include <sys/types.h>
#include < sys/socket.h >         
ssize_t send(int sockfd, const void *buf, size_t len, int flags);  
  回:返回写出的字节数---成功   -1---失败
 -------------------------------------------------------------------

前3个参数与write()相同,参数flags是传输控制标志。传输控制标志是什么?反正一般为0.

(2)recv()函数用于数据的发送。

-------------------------------------------------------------------
#include <sys/types.h>
#include < sys/socket.h >         
ssize_t recv(int sockfd, void *buf, size_t len, int flags);  
  回:返回读入的字节数---成功   -1---失败
 -------------------------------------------------------------------

前3个参数与read()相同,参数flags是传输控制标志。

客户端:
socket() --> connect() --> send() --> close()
创建socket --> 连接指定服务器的IP/端口号–>向socket中写入信息–>关闭socket

当用socket建立了套接口后,紧接着调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。connect也是可以用于UDP的。

-----------------------------------------------------------------
 #include <sys/socket.h>      
  int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);  
           返回:0---成功   -1---失败
 -----------------------------------------------------------------

结构体是定义在<netinet/in.h>里面的。TCP需要连接,UDP的话就直接在sendto和recvfrom中填入信息就可以了。

实际上对于TCP,它即便是连接,他也是一次性的。。。每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程

现在只是单个连接,就假定它一个服务器只连接一个客户端。

对于 TCP 客户端编程流程,有点类似于打电话过程:找个可以通话的手机(socket() ) -> 拨通对方号码并确定对方是自己要找的人( connect() ) -> 主动聊天( send() 或 write() )-> 或者,接收对方的回话( recv() 或 read() )-> 通信结束后,双方说再见挂电话(close() )。记住这个比喻很有用。

做为 TCP 服务器需要具备的条件呢?

具备一个可以确知的地址( bind() ):相当于我们要明确知道移动客服的号码,才能给他们电话;

让操作系统知道是一个服务器,而不是客户端( listen() ):相当于移动的客服,客服把自己的手机号码上报给公司,他们主要的职责是被动接听用户电话,而不是主动打电话骚扰用户;而且移动给它规定了能同时接听多少个客户端(用于队列设置)

等待连接的到来( accept() ):移动客服时刻等待着,来一个客户接听一个。

接收端使用 bind() 函数,来完成地址结构与socket 套接字的绑定,这样 ip、port 就固定了,发送端即可发送数据给有明确地址( ip+port ) 的接收端。

对于 TCP 服务器编程流程,有点类似于接电话过程:移动分配一个手机(socket() ) -> 自己上报给移动自己的手机号( bind() ) -> 移动给客服派发任务,职责为被动接听,同时能接收多少个( listen() ) -> 有来电,确定双方的关系后,才真正接通不挂电话( accept() ) -> 接听对方的诉说( recv() ) -> 适当给些回话( send() )-> 通信结束后,双方说再见挂电话( close() )。

既然是这样的例子,那么我的理解是,如果一方挂断电话,那么双方都无法通信了,因此,通电话的例子并不能完美的表达,自从listen开始,就已经维护好了一个队列,任何客户端都可以往这个队列里发送数据包,accept负责解析,这样的话,服务器端关闭客户端sock就是不必要的,很可能这里出错。

但是感觉又不想,它就是只能有一次,二者不能持续通信。

三种模式:

单执行流的Server端每次只能和一个Client端连接

多进程的Server能同时和多个Client连接,但开销较大

多线程的Server用多个线程的方式同时和多个Client端连接,我的Server端就采用的是这种模式

也就是说,简单TCP只能连接一个客户端。。。但是持续通信跟这个没关系啊。。。

 

每日一个知识点

TCP协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个MSL的时间才回到CLOSE的状态。

MSL的具体时间由操作系统决定,通常 server主动关闭2分钟后 就可以再次启动了

当然,如果等不及的话,也可以用setsockopt,:

查找某个进程,用

ps aux | grep xxx