socket网络编程基础篇-------如何写一个简单的TCP服务器

 

目录

socket通信的预备知识  

1.什么是socket通信?​

2.什么是字节序?

3.什么是IP地址转换?

4.什么是网络地址格式?

5.什么是“半关闭”?

6.为什么会用到"半关闭"? 

socket通信的具体过程

1.概述

2.socket函数

3.bind函数

4.listen函数,connect函数,accept函数

5.send函数,sendto函数,sendmsg函数

6.recv函数,recvfrom函数,recvmsg函数

7.close函数 shutdown函数

socket通信的实例(TCP和UDP服务器)

tcp服务器

tcp客户端

udp服务器

udp客户端

参考



刚刚过去的研一上学期,
一直在恶补cs的基础知识,同时也在实验室做了一些小项目,
由于平常习惯把学的东西整理思维导图xmind,趁寒假有时间,
打算接下来把这几个月学习的一些东西从思维导图整理到博客,算是一次梳理和复习。
(包括操作系统,计算机网络,git,linux命令,数据结构,医学图像处理库ITK vtk,cmake,QT,javascript的以及一些项目)
由于最近在学习apue的后面socket通信部分,所以就从这里开始吧 。
 

 

socket通信的预备知识  

1.什么是socket通信?

我们知道操作系统中一个主机中进程通信方式有比如管道 信号 共享内存等等,
那通过网络相连的不同主机间的进程该如何通信呢?(比如我电脑上的一个QQ要给你电脑上的一个QQ发消息)
其中的一个机制是socket套接字(其实socket也可以支持同一主机下的进程间通信)
在计算机网络中我们知道,支持网络间通信的主流协议是TCP/IP协议
所以socket通信也得依据这个协议,在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,
IP地址指定了网络中是哪一台计算机 端口号指定了该计算中的哪一个进程
所以socket绑定一个“IP地址+端口号”就可以实现网络中两个特定主机间的特定进程通信

socket网络编程基础篇-------如何写一个简单的TCP服务器_第1张图片

2.什么是字节序?

  • 字节序有哪些类型?

所谓的字节序我理解就是 数据在计算机内存中的不同存储方式 它有两种:小端序和大端序

计算机内部进行运算的时候都是沿着内存地址增大的方向逐个读取字节然后运算的,
而一般运算的时候都是先进行低位运算再进行高位运算效率会比较高,
比如我们要乘法运算时都会从低位开始相乘往在一个个往高位算 计算机内部也是这个道理

所以就得保证在计算机沿着内存地址增大的方向逐个读取数据时,是先读取低位字节再读取高位字节

比如有一个十六进制的数据0x1234 其中0x12是高字节 0x34是低字节,为了保证计算的高效

它在内存中存储的时候得是在内存地址小的地方是低字节0x34 在内存大的地方是高字节0x12 

这种数据在内存中的存储方式就是小端序  

下面介绍与小端序相反的大端序

比如有一个十六进制的数据0x1234 其中0x12是高字节 0x34是低字节
它在内存中存储的时候得是在内存地址小的地方是高字节0x12 在内存大的地方是低字节0x34

这就是大端序
计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。
除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

socket网络编程基础篇-------如何写一个简单的TCP服务器_第2张图片

  • 为什么需要字节序之间的转换?
    目前常见的计算机内部处理器架构都是小端字节序 而TCP/IP协议所规定的网络字节序是大端字节序
    所以在网络传输时就需要对字节序进行转换
    socket网络编程基础篇-------如何写一个简单的TCP服务器_第3张图片
  • 字节序转换如何实现?
    根据APUE一书所写,UNIX操作系统为字节序转换提供了四个函数
    #include 
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    h表示host,n表示network,l表示32位长整数,s表示16位短整数。
    如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,
    如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
            #include 
     	#include 
     	int main()
     	{
    	    int i_num = 0x12345678;
    	    printf("[0]:0x%x\n", *((char *)&i_num + 0));
     	    printf("[1]:0x%x\n", *((char *)&i_num + 1));
     	    printf("[2]:0x%x\n", *((char *)&i_num + 2));
     	    printf("[3]:0x%x\n", *((char *)&i_num + 3));
     	 
     	    i_num = htonl(i_num);
     	    printf("[0]:0x%x\n", *((char *)&i_num + 0));
     	    printf("[1]:0x%x\n", *((char *)&i_num + 1));
     	    printf("[2]:0x%x\n", *((char *)&i_num + 2));
    	    printf("[3]:0x%x\n", *((char *)&i_num + 3));
     	 
     	    return 0;
    	} 
    
    在80X86CPU平台上,执行该程序得到如下结果: 
    [0]:0x78 
    [1]:0x56 
    [2]:0x34 
    [3]:0x12
    
    [0]:0x12 
    [1]:0x34 
    [2]:0x56 
    [3]:0x78

     

3.什么是IP地址转换?

通常我们认识的ip地址 是192.168.6.250这样的格式,这种格式叫点分十进制
而在网络编程中我们常常需要把这种ip地址转换成二进制格式去处理
也就是说我们需要一组函数实现点分十进制的ip地址转换成二进制的ip地址格式
于是在unix中引入了一组函数去实现这样的操作

#include

int inet_pton(int family, const char *strptr, void *addrptr);
                                    
返回:若成功则为1,若输入不是有效的表达格式则为0,若出错则为-1
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
                                               
返回:若成功则为指向结果的指针, 若出错则为NULL

首先说第一个函数 inet_pton:
这个函数功能是将点分十进制的字符串格式ip转换成网络传输中的二进制格式ip 
family参数只能是是宏定义AF_INET,或者AF_INET6。分别表示要转换的是ipv4或者ipv6,传入其他参数将返回一个错误,并将errno置为EAFNOSUPPORT
strptr表示指向被转换的字符串形式的点分十进制ip
addrptr表示存储转换后的二进制格式的ip
再说第二个函数inet_ntop
family,strptr参数,addrptr功能同上
说一说len参数,len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。
可以用在头文件中进行的宏定义

#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

分别表示存放ipv4地址的空间大小和存放ipv6地址的的空间大小
也可以自行去定义len的大小,但是如果len太小,不足以容纳表达式结果(包括结尾的空字符),
那么返回一个空指针,并置errno为ENOSPC
同时inet_ntop函数的strptr参数不可以是一个空指针。
调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

4.什么是网络地址格式?

  • sockaddr

前面说到,一个socket套接字必须绑定一个ip地址和端口号才能实现特定主机中的特定进程间的通信
而在unix中表示ip地址和端口号并不是两个变量分开来表示,而是整合到一个结构体
这个结构体就是sockaddr(如下)。
socket网络编程基础篇-------如何写一个简单的TCP服务器_第4张图片
socket网络编程基础篇-------如何写一个简单的TCP服务器_第5张图片

sockaddr结构体在#include 中定义,
sa_family表示一个地址族 

sa_family地址族 表示一般都是“AF_xxx”的形式,
它的值包括三种:AF_INET,AF_INET6和AF_UNSPE。
AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型,
AF_INET6 则是 IPv6 的;
而 AF_UNIX 则是 Unix 系统本地通信。


sa_data[14]存放ip地址和端口号
总共的大小是16字节
在没有引入ipv6之前,是一直使用sockaddr这个结构体的,在一些要用到ip地址和端口号的函数(比如绑定的bind函数)中 
直接把这个结构体传进去就行了,但是引入了ipv6以后,如果ipv4和ipv6都用这个结构体表达就会有些混乱
但是如果重新引入新的结构体去分别表达ipv4和ipv6就会出现新的问题,
就是一些用到ip地址端口号的函数原来都是用sockaddr结构体作为参数传入的
引入了新的表达方式 为了用上这些新的表达方式下的结构体,可能这些函数(比如bind)的底层就得重新写,很麻烦
于是就想了一个办法引入还是得引入 但是就是向这些函数传递参数时用强制转换的方式把新引入的结构体转换成原来的结构体
以bind函数为例子 
bind函数的参数定义是这样的
注意它的第二个参数是一个指向sockaddr结构体的指针addr
现在真正使用时会用强制转换运算符把传进去的参数进行转换

比如下面的servaddr可能是新引入的表示ipv4或者ipv6的结构体

  • sockaddr_in

新引入的iPv4和IPv6的结构体=定义在头文件netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,
它的地址族用常数AF_INET表示 数据类型in_port_t定义成uint16_t ,in_addr_t定义成uint32_t
socket网络编程基础篇-------如何写一个简单的TCP服务器_第6张图片


该结构体的赋值方法socket网络编程基础篇-------如何写一个简单的TCP服务器_第7张图片

  • sockaddr_in6

IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。 

socket网络编程基础篇-------如何写一个简单的TCP服务器_第8张图片

5.什么是“半关闭”?

上面说到socket通信主要针对的是网络中两个不同计算机上的两个不同进程之间的通信,
这样一种通信通常用一种服务器—客户端模型去表达,服务器和客户端都可以看作是网络中两个不同的计算机
只不过他们的功能侧重有所不同,服务器主要用来"响应",而客户端主要进行”请求“
socket通信主要是在他们俩之间搭建一个桥梁,
而他们之间的通信过程可以概括为开始连接—>数据交换——>关闭连接这样一个过程
其中开始连接过程通常不会发生大的变故,而关闭连接过程往往会发生意想不到的状况,所以重点说说关闭连接的一个过程。
把服务器和客户端看成是史前不存在货币交换时期的两个村子,这两个村子之间要交换货物,但是两个村子之间隔着一座座山
开始连接的过程就相当于在两个村子之间凿开两条路 但是这两个村子的人规定好了这两条路都是单向的 
一条路只能从服务器村到客户端村运货物 一条路只能从客户端村到服务器村运货物
实际上对应两个数据流
每条路在两个村里都只有一个入口 两条路就两个入口
数据交换过程就是两个村子之间通过这两条路来交换货物,
                                     注意每个村子发货物时是通过先把货物打包好放在发货仓库中(缓存区)
                              再把仓库中的货一下子发过去的(write/send函数)
                                                                          每个村子收货物时也是对面有货来了就收走放在收货仓库(缓存区)
                                                                                                            再把仓库中的货一下子读出来(read/recv函数)
关闭连接过程就是暂时两个村货物交换完了,通过堵住自己村里这两条路的入口来关闭货物交换 这个时候就分两种情况
       一种情况是"全面关闭" 把两个门都堵住:村子的人既不想自己的人出去,也不想对面的人过来。
       一种情况是半关闭  把一个门堵住:村子里面的人要么只出不进,要么只进对面的人不能出自己的人。
                    下面会说到 全面关闭用的是close函数 半关闭用的是shutdown函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第9张图片

6.为什么会用到"半关闭"? 

按照上面的说明好像把两个门都堵住的全面关闭才是更好的做法,但实际上半关闭有更重要的用途
在《TCP/IP网络编程》上面是这样讲的

socket网络编程基础篇-------如何写一个简单的TCP服务器_第10张图片

总结一下就是,在这样一种情况时:
服务器与客户端传文件时,想要达到的效果是 在客户端收到完整的文件给服务器反馈一个消息(比如字符串“Finished”  )
这其中其实可以分解为两步:step1:收到完整的文件以后客户端停止读取 step2:  停止读取以后给服务器发消息“Finished”
为了实现step1 ,现在的想法是
方案一:给文件中加某个标识符 在客户端读取到这个标识符时就停止读取
    方案一不可行,因为可能文件中会存在和标识符相同的字符,导致客户端没读完就停了
方案二,利用close函数全面关闭数据流通道时所产生的EOF(End OF File)标识符,客户端在读取到这个标识符时会停止
               方案二看起来可行,实际上会带来另外一个问题,就是用close全面关闭了以后,客户端就无法返回消息“Finished”
              所以也不可行
方案三:用shutdown函数实现半关闭,即关闭一个数据传输流,保留一个数据传输流,这样就能保证既能产生EOF标识符
    让客户端停止读取 又能够保证客户端能够返回消息(因为还有一条数据传输流)
这就是半关闭的用武之地
具体的代码例子可以看下面的close函数shutdown函数那一节

 


 

socket通信的具体过程

1.概述

  • 使用tcp协议进行的有连接网络通信要经过下面这些流程 

socket网络编程基础篇-------如何写一个简单的TCP服务器_第11张图片

1.服务器先用 socket 函数来建立一个套接字,用这个套接字完成通信的监听。 
2.用 bind 函数来绑定一个端口号和 IP 地址。因为本地计算机可能有多个网址和 IP,每一个 IP 和端口有多个端口。
需要指定一个 IP 和端口进行监听。 
3.服务器调用 listen 函数,使服务器的这个端口和 IP 处于监听状态,等待客户机的连接。 
4.客户机用 socket 函数建立一个套接字,设定远程 IP 和端口。 
5.服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于
监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服
务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK
段,服务器收到后从accept()返回。

6.建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是
由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()
返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客
户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处
理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发
回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一
条请求,如此循环下去。
7.如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,
服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注
意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方
调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

 无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有建立连接的过程。进行通信之前,需要建立网络套接字。服务器需要绑定一个端口,在这个端口上监听接收到的信息。客户机需要设置远程 IP 和端口,需要传递的信息需要发送到这个 IP 和端口上。 由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很
多保证通讯可靠性的机制需要在应用层实现。

 

  • 使用udp协议的无连接网络通信要经过下面这些流程 

socket网络编程基础篇-------如何写一个简单的TCP服务器_第12张图片  

2.socket函数

上面介绍了tcp协议下和udp协议下,用socket通信的具体过程,下面将逐个讲讲上面所用到的API,先从socket函数讲起。 

 用socket函数表示通信端点的抽象,用Socket函数创建一个套接字
 

#include 
#include 
int socket(int domain, int type, int protocol);

可以看到socket只有三个参数 下面分别说说这三个参数的功能
domain参数:表示是要用socket进行本机域之间的通信(AF_UNIX)还是在网络域之间的通信,
                 如果是网络域则是用ipv4(AF_INET )还是ipv6(AF_INET6)

AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
表示各个域的常数常常以AF_开头,表示地址族
type参数:表示指定了在哪个域通信以后,用哪个协议去完成通信,
                一般用Tcp协议(SOCK_STREAM)或UDP协议(SOCK_DGRAM)

SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的
接受才能进行读取。
SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使
用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数
据包的顺序
protocol参数:该参数的介绍从略,指定为0即可。
0 默认协议
返回值:
成功返回一个新的套接字描述符,失败返回-1,设置errno 。套接字描述符本质上是一个文件描述符,在UNIX的文件IO函数中有很多以文件描述符为参数的函数,那是不是这些函数接收套接字描述符会有相同的效果呢?并非全部如此,下表给出了大多数以文件描述符为参数的函数用套接字描述符作为参数时候的行为(摘自APUE)
socket网络编程基础篇-------如何写一个简单的TCP服务器_第13张图片

 

3.bind函数

根据客户机/服务器模型(C/S模型),客户机必须要知道服务器的ip和端口才能够进行连接 ,
所以服务器就必须绑定一个ip和端口,这个绑定就通过bind函数来完成
bind()的作用是将参数表示socket的文件描述符sockfd和表示IP地址和端口号的结构体addr绑定在一起,
使sockfd这个用于网络通讯的文件
描述符监听addr所描述的地址和端口号。
前面讲过,struct sockaddr *是一个通用指针类型,
addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,
所以需要第三个参数addrlen指定结构体的长度。 
 

#include 
#include 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度
返回值:
成功返回0,失败返回-1, 设置errno

4.listen函数,connect函数,accept函数

由于这三个函数联系比较紧密,故放在一起讲解。 

listen()函数是针对服务器的,它有两个作用
作用一.服务器是被动连接的,listen函数把服务器套接字变成被动监听的状态,相当于是10086的客服(服务器)等待着客户(客户端)电话的到来
作用二:listen函数中有一个参数backlog,它在内核中设定了一个叫"连接队列"的东西的长度,每次来一个连接就放到连接队列中等待后续操作,当按照设定的长度连接队列满了以后,客户端再次请求连接时服务器就忽略客户机的请求。 
注意:listen函数是非阻塞的,也就是说它运行完以后就继续往下执行了,不会一直堵在那。

connect()函数是针对客户端的,该函数的功能就是对服务器建立三次握手连接,,连接后放入上面所说的“连接队列”
注意:连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。
 

accept()函数作用是从处于 连接队列的头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。如果服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器就会忽略客户端的connect()连接请求,所以客户端的 connect 就会返回 ETIMEDOUT。

上面大概说了这三个函数的作用 下面说说他们的函数接口具体长什么样

listen()函数

socket网络编程基础篇-------如何写一个简单的TCP服务器_第14张图片
注意:   listen()成功返回0,失败返回-1。
          写程序时服务器的 listen() 的backlog参数最好还是根据需要填写,
           写太大不好,浪费资源,写太小也不好,延时建立连接。
       (具体可以看cat /proc/sys/net/core/somaxconn,默认最大值限制是 128)

查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog

connect()函数 
socket网络编程基础篇-------如何写一个简单的TCP服务器_第15张图片
accept()函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第16张图片

 

5.send函数,sendto函数,sendmsg函数

前面讲到,可以把套接字描述符看成一种文件描述符,从而就可以使用文件io读写函数read和write进行数据读写 。
但是应用和两个函数用在套接字上有它自身的局限性,
因为他并不是为套接字的数据收发量身打造的,所以不能实现很多必要的功能比如发送带外数据,从多个客户端发送等等
于是对于套接字来说有send ,recv等函数的出现。对于发送端来说三个函数send函数,sendto函数,sendmsg函数。
send函数

socket网络编程基础篇-------如何写一个简单的TCP服务器_第17张图片

sendto函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第18张图片

sendmsg函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第19张图片

send工作过程

执行流程:

这里只描述同步Socket的send函数的执行流程。

第一阶段:等待内核把手头的数据发完

当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的 长度,

如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;

如果len小于或者等于s的发送缓冲区的长度,

那么send先检查内核是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,

如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,

那么 send就比较s的发送缓冲区的剩余空间和len,

如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,

在Unix系统下,如果send在等待内核传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。send函数也返回SOCKET_ERROR。

如果len小于剩余 空间大小send就仅仅把buf中的数据copy到剩余空间里

如果send函数copy数据成功,就返回实际copy的字节数,

如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;

 

 

要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,

但是此时这些数据并不一定马上被传到连接的另一端。

如 果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。

(每一个除send外的Socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,

如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)


总结 
发送消息,send只可用于基于连接的套接字,send 和 write唯一的不同点是标志的存在,当标志为0时,send等同于write。sendto 和 sendmsg既可用于无连接的套接字,也可用于基于连接的套接字。除了套接字设置为非阻塞模式,调用将会阻塞直到数据被发送完。
 

用法:
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sock, const void *buf, size_t len, int flags);
ssize_t sendto(int sock, const void *buf, size_t len, int flags,const struct sockaddr *to, socklen_t tolen);
ssize_t sendmsg(int sock, const struct msghdr *msg, int flags);

参数:  

sock:索引将要从其发送数据的套接字。
buf:指向将要发送数据的缓冲区。
len:以上缓冲区的长度。
flags:是以下零个或者多个标志的组合体,可通过or操作连在一起

MSG_DONTROUTE:不要使用网关来发送封包,只发送到直接联网的主机。这个标志主要用于诊断或者路由程序。
MSG_DONTWAIT:操作不会被阻塞。
MSG_EOR:终止一个记录。
MSG_MORE:调用者有更多的数据需要发送。
MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。
MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持此种操作。

to:指向存放接收端地址的区域,可以为NULL。
tolen:以上内存区的长度,可以为0。
msg:指向存放发送消息头的内存缓冲,结构形态如下

struct msghdr {
    void         *msg_name; 
  socklen_t     msg_namelen;   
    struct iovec *msg_iov;       
    size_t        msg_iovlen;    
    void         *msg_control;   
    socklen_t     msg_controllen;
    int           msg_flags;     
};


可能用到的数据结构有

struct cmsghdr {
    socklen_t cmsg_len;    
    int       cmsg_level;  
    int       cmsg_type;   
   
};
返回说明:  
成功执行时,返回已发送的字节数。失败返回-1,errno被设为以下的某个值  
EACCES:对于Unix域套接字,不允许对目标套接字文件进行写,或者路径前驱的一个目录节点不可搜索
EAGAIN,EWOULDBLOCK: 套接字已标记为非阻塞,而发送操作被阻塞
EBADF:sock不是有效的描述词
ECONNRESET:连接被用户重置
EDESTADDRREQ:套接字不处于连接模式,没有指定对端地址
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
EISCONN:基于连接的套接字已被连接上,同时指定接收对象
EMSGSIZE:消息太大
ENOMEM:内存不足
ENOTCONN:套接字尚未连接,目标没有给出
ENOTSOCK:sock索引的不是套接字
EPIPE:本地连接已关闭

6.recv函数,recvfrom函数,recvmsg函数

recv函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第20张图片

 socket网络编程基础篇-------如何写一个简单的TCP服务器_第21张图片

recvfrom函数
socket网络编程基础篇-------如何写一个简单的TCP服务器_第22张图片

recvmsg函数 
socket网络编程基础篇-------如何写一个简单的TCP服务器_第23张图片

 

recv执行流程 

执行流程:

这里只描述同步Socket的recv函数的执行流程。

第一阶段 等待内核把数据准备好:

当应用程序调用recv函数时,如果sockfd接收缓冲区中没有数据或者协议正在接收数据,

那么recv就一直等待,只到 协议把数据接收完毕。

第二阶段 从内核缓冲区拷贝数据到自己的缓冲区

当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中

recv函数返回其实际copy的字节数。

 

如果recv在copy时出错,那么它返回SOCKET_ERROR;

如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,


7.close函数 shutdown函数

close()函数

#include
int close(int sockfd);     //返回成功为0,出错为-1.
    close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

    在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。

shutdown()函数

#include
int shutdown(int sockfd,int howto);  //返回成功为0,出错为-1.
    该函数的行为依赖于howto的值

    1.SHUT_RD:值为0,关闭连接的读这一半。

    2.SHUT_WR:值为1,关闭连接的写这一半。

    3.SHUT_RDWR:值为2,连接的读和写都关闭。

    终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。

两函数的区别
    close与shutdown的区别主要表现在:
    close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。

    而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。

    下面将展示一个客户端例子片段来说明使用close和shutdown所带来的不同结果:

     客户端有两个进程,父进程和子进程,子进程是在父进程和服务器建连之后fork出来的,子进程发送标准输入终端键盘输入数据到服务器端,知道接收到EOF标识,父进程则接受来自服务器端的响应数据。

   /* First  Sample client fragment,
    * 多余的代码及变量的声明已略       */
      s=connect(...);
      if( fork() ){   /*      The child, it copies its stdin to the socket              */
          while( gets(buffer) >0)
              write(s,buf,strlen(buffer));
              close(s);
              exit(0);
      }
      else {          /* The parent, it receives answers  */
           while( (n=read(s,buffer,sizeof(buffer)){
               do_something(n,buffer);
               /* Connection break from the server is assumed  */
               /* ATTENTION: deadlock here                     */
            wait(0); /* Wait for the child to exit          */
            exit(0);
       }
 
    对于这段代码,我们所期望的是子进程获取完标准终端的数据,写入套接字后close套接字,并退出,服务器端接收完数据检测到EOF(表示数据已发送完),也关闭连接,并退出。接着父进程读取完服务器端响应的数据,并退出。然而,事实会是这样子的嘛,其实不然!子进程close套接字后,套接字对于父进程来说仍然是可读和可写的,尽管父进程永远都不会写入数据。因此,此socket的断连过程没有发生,因此,服务器端就不会检测到EOF标识,会一直等待从客户端来的数据。而此时父进程也不会检测到服务器端发来的EOF标识。这样服务器端和客户端陷入了死锁(deadlock)。如果用shutdown代替close,则会避免死锁的发生。

     if( fork() ) {  /* The child                    */
           while( gets(buffer)
              write(s,buffer,strlen(buffer));
           shutdown(s,1); /* Break the connection
                             *for writing, The server will detect EOF now. Note: reading from
                          *the socket is still allowed. The server may send some more data
                         *after receiving EOF, why not? */
           exit(0);
      }

 

socket通信的实例(TCP和UDP服务器)

tcp服务器

/* server.c */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
/*变量定义*/
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
/*创建套接字socket()*/
listenfd = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址结构体*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
/*给套接字绑定地址bind*/
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
/*使套接字处于监听状态listen*/
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) 
{
/*接受客户端的请求accept*/
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
/*读取客户端发来的数据read*/
read(connfd, buf, MAXLINE);

/*处理客户端发来的数据*/
printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
{
  buf[i] = toupper(buf[i]);
}

/*向客户端发送请求write*/
write(connfd, buf, n);

/*关闭连接close*/
close(connfd);
}
}


 

tcp客户端
 

/* client.c */
#include 
#include 
#include 
#include 
#include 
#include 
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
/*变量初始化*/
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2) {
fputs("usage: ./client message\n", stderr);
exit(1);
}
str = argv[1];

/*创建套接字*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址结构体*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
/*连接服务器*/
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

/*向服务器发送数据*/
write(sockfd, str, strlen(str));

/*读取服务器发过来的数据*/
read(sockfd, buf, MAXLINE);

printf("Response from server:\n");
/*在显示器显示数据*/
write(STDOUT_FILENO, buf, n);
/*关闭连接*/
close(sockfd);
return 0;
}

udp服务器
 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main(void)
{
    /*变量声明*/
	char buf[80];//数据接收缓存区
	struct sockaddr_in client_addr;//指向客户端发送方的套接字
	socklen_t client_len;//存储客户端发送方的缓存区大小
	char str[INET_ADDRSTRLEN];//存储转换后的字符串形式点分十进制ip
    /*创建套接字socket()*/

       int udp_server=socket(AF_INET,SOCK_DGRAM,0);//创建一个ipv4下udp协议的套接字
    /*绑定ip和端口bind()*/
        struct sockaddr_in udp_addr;//创建一个ipv4地址结构体
	bzero(&udp_addr,sizeof(udp_addr));//用bzero函数清零
	udp_addr.sin_family=AF_INET;//将结构体内的地址族设置为AF_INET
	udp_addr.sin_addr.s_addr=htonl(INADDR_ANY);//设置本机内任意一个网卡ip
	udp_addr.sin_port=htons(8000);//设置端口号为8000
	bind(udp_server,(struct sockaddr *) &udp_addr,sizeof(udp_addr));//绑定套接字和地址结构体
   	printf("开始监听...\n");

	while(1)
	{
    /*接收数据recvfrom()*/
	client_len = sizeof(client_addr);
	int n=recvfrom(udp_server,buf,80,0,(struct sockaddr *)&client_addr,&client_len);  
    /*处理数据*/
	printf("接收到来自 %s : %d 的请求\n",inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),ntohs(client_addr.sin_port));
	for (int i = 0; i < n; i++)
		buf[i] = toupper(buf[i]);//把小写的字符转换成大写的字符
   /*响应数据sendto()*/
	n = sendto(udp_server, buf, n, 0, (struct sockaddr *)&client_addr, sizeof(client_addr));
	}

    /*关闭套接字close()*/
	close(udp_server);
	return(0);
}

udp客户端

/* udp client.c */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{

/*变量初始化*/
struct sockaddr_in servaddr;
int sockfd, n;
char buf[80];

/*创建套接字socket()*/
sockfd = socket(AF_INET, SOCK_DGRAM, 0);

/*设定ip和端口*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(8000);

while (fgets(buf, 80, stdin) != NULL)
 {

/*发送数据sendto*/
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

/*接收服务器的反馈recvfrom*/
n = recvfrom(sockfd, buf, 80, 0, NULL, 0);

/*在显示器上显示服务器发回的数据write*/
write(STDOUT_FILENO, buf, n);
}
/*关闭套接字close*/
close(sockfd);
return 0;
}

代码见Github:https://github.com/KyleAndKelly/LearnSocket

 

 


参考

1.《UNIX环境高级编程》

2.https://www.cnblogs.com/gremount/p/8830707.html 

3.https://www.cnblogs.com/loanhicks/p/7368151.html

4.https://blog.csdn.net/someday1314/article/details/79986774

5.https://blog.csdn.net/tennysonsky/article/details/45621341

6.http://blog.chinaunix.net/uid-28455968-id-4140112.html

7.https://blog.csdn.net/lgp88/article/details/7176509

 

 

 

 

你可能感兴趣的:(网络编程)