第一步:调用socket 网络API,创建套接字文件,socket 函数详细说明(TCP/IP网络编程)linux(zzt)

  • TCP通信的代码举例
  • 实现TCP服务器程序
    • 第1步:调用socket 网络API,创建套接字文件
      • socket函数
        • 函数原型
        • 功能
        • 返回值
        • 参数
          • domian 指定协议族
          • type 套接字类型
          • protocol 指定协议号
  • 代码演示

TCP通信的代码举例

· 写一个TCP服务器程序
· 写一个客户端程序
然后客户端向服务器请求连接,当连接成功之后,服务器和客户端程序实现通信。

不过为了不要让例子太复杂,服务器程序目前只为一个客户服务,至于使用多进程、多线程以及多路io为多客户服务的情况,后面进行说明。

实现TCP服务器程序

按照TCP编程模型来编写

第1步:调用socket 网络API,创建套接字文件

socket函数

函数原型

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

功能

创建一个套接字文件,然后以文件形式来操作通信,不过套接字文件没有文件名。

Linux有7种文件,套接字文件就是其中一种。

socket翻译为中文就是“套接字”的意思,其实翻译为插座更合适些,因为socket本来就有插座的意思,使用socket创建一个通信的套接字文件,就好比给应用程序插上了一个通信的插座,有了这个插座就可以和对方应用程序通信了。

图解说明:

socket函数说明

返回值

  • 成功:返回套接字文件描述符。
  • 失败:返回-1,errno被设置。

参数

domian 指定协议族

族/范围

1
int socket(int domain, int type, int protocol);
  • 作用:指定协议族

为什么要指定协议族?
因为你要使用的通信协议一定是属于某个协议族,所以如果不指定协议族,又怎么指定协议族中的某个具体协议呢。
比如我们想用的是TCP协议,TCP属于TCP/IP协议族中的子协议,所以必须先通过domain指定TCP/IP协议族,不过TCP/IP协议族有两个版本,分别是IPV4是IPV6版本,我们目前使用的还是IPV4版本,因为Ipv6还未普及。

IPV4是Internet Protocol Version4的缩写,直译为“网络协议第四版”,IPV4和IPV6这两个版本所使用的ip地址格式完全不同,
IPV4:ip为32位
IPV6:ip为128位

不仅IPV4和IPV6的ip地址格式不同,其实所有不同网络协议族所使用ip地址格式都不相同。

1
int socket(int domain, int type, int protocol);
  • domain可设置的常见宏值。

可设置的有:AF_UNIX, AF_LOCAL(前面两个宏明表示的意义是一样的)、AF_INET、AF_INET6AF_IPXAF_NETLINKAF_APPLETALKAF_PACKET、AF_UNSPEC,有关每个宏的作用后面再解释,这里先介绍下这些“宏名”。

AF是address family,表示地址家族的意思,由于每个网络协议族的ip地址格式完全不同,因此在指定ip地址格式时需要做区分,所以这些AF_***宏就是用于说明所使用的是什么协议族的IP地址,也就是说定义这些宏的目的是用来区分不同协议族的IP地址格式。

这些个宏定义在了socket.h中
我们下面给出一部分宏名对应的内容。

1
2
3
4
5
6
#define AF_UNSPEC   0
#define AF_UNIX     1   /* Unix domain sockets      */
#define AF_LOCAL    1   /* POSIX name for AF_UNIX   */
#define AF_INET     2   /* Internet IP Protocol     */
#define AF_AX25     3   /* Amateur Radio AX.25      */
...

有读者可能会说不对呀
domain不是用来指定协议族的吗,但是AF_ * * * 确是用来区分不同协议ip格式的,给domain指定AF_ * * * 合适吗?

其实是合适的,没有问题。实际上区分不同协议族应该使用PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC,PF就是protocol family的意思,意思是“协议家族”。

PF_***定义的宏与AF_***定义的宏不同的只是前缀,不过AF_***宏与PF_***宏对应的值完全一样,比如AF_UNIX == PF_UNIX,所以给domain指定AF_***,与指定PF_***完全是一样的。

为什么AF_ * * * 定义的宏 == PF_ * * * 定义的宏?

AF_***用于区分不同协议族的ip地址格式,而PF_***则用于区分不同的协议族,但是每个协议族的IP格式就一种,所以协议族与自己的IP格式其实是一对一的,因此如果你知道使用的是什么ip地址格式,其实你也就知道了使用的是什么协议族,所以使用AF_***定义的宏也可以用于区分不同的协议族。

不过为了更加正规一点,区分不同协议族的宏还是被命名为了PF_***,只不过它的值就是AF_***的值。

1
2
3
4
5
6
#define PF_UNSPEC   AF_UNSPEC
#define PF_UNIX     AF_UNIX
#define PF_LOCAL    AF_LOCAL
#define PF_INET     AF_INET
#define PF_AX25     AF_AX25
...
1
int socket(int domain, int type, int protocol);

总之希望读者理解,domain是用于指定协议族的,设置的宏可以是AF_***,也可以是PF_***,不过正规一点的话还是应该写PF_***,因为这个宏才是专门用来区分协议族的,而AF_***则是用来区分不同协议族的ip地址格式的。

不过socket的手册里面写的都是AF_***,没有写PF_***。

socket手册查看

  • domain的常见宏值,各自代表是什么协议族,制定不同的宏就代表不同的协议族。

AF_UNIX, AF_LOCAL、AF_INET、AF_INET6、AF_IPX、AF_NETLINK、AF_APPLETALK、AF_PACKET、AF_UNSPEC。
上面宏值与下面的宏值对应相等。
PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC。

  • PF_UNIX、PF_LOCAL:域通信协议族
    这两个宏值是一样(宏值都是1)。

给domain指定该宏时就表示,要使用的是“本机进程间通信”协议族,域通信也就是本机进程间通信,我们在说明IPC时就说过,还有一种域套接字的IPC,也可以专门用来实现“本机进程间通信”,域套接字IPC就是我们这里说明的域通信。

这里域就是本机的意思,当我们给socket的domain指定这两个宏时,创建的就是域套接字文件,这个套接字文件是用来实现域通信的,所以把创建的文件就称为域套接字文件。
后面说明域套接字通信时,会具体的来使用这个宏。

  • PF_INET:指定ipv4的TCP/IP协议族。
    我们这篇博客要说明的TCP和UDP就是IPV4的TCP和UDP。

  • PF_INET6:ipv6的TCP/IP协议族,目前还未普及使用,我们这里不说明。

  • PF_IPX:novell协议族,几乎用不到,了解即可。

novell协议族由美国Novell网络公司开发的一种专门针对局域网的“局域网通信协议”。

这个协议的特点是效率较高,所以好多局域网游戏很喜欢使用这个协议来进行局域网通信,比如以前的局域网游戏CS,据说使用的就是novell协议族。

之所以使用novell协议族,是因为CS的画面数据量太大,而且协同性要求很高,所以就选择了使用novell协议族这个高效率的局域网协议。

现在互联网使用的都是TCP/IP协议,而novell和TCP/IP是两个完全不同的协议,所以使用novell协议族的局域网与使用TCP/IP协议族的互联网之间兼容性比较差,如果novell协议的局域网要接入TCP/IP的Internet的话,数据必须进行协议转换。

所谓协议转换就是
novell局域网的数据包发向TCP/IP的互联网时,将novell协议族的数据包拆包,然后重新封包为TCP/IP协议的数据包。

TCP/IP的互联网数据包发向novell局域网时,将TCP/IP协议族的数据包拆包,然后重新封包为novell协议的数据包。

windows似乎并不是支持novell协议,但是Linux、unix这边是支持的。

我怎么知道是支持的?
既然我们在socket手册中查到了PF_IPX,就说明时肯定支持。
socket手册查看

  • PF_APPLETALK:苹果公司专为自己“苹果电脑”设计的局域网协议族。

  • AF_UNSPEC:不指定具体协议族
    int socket(int domain, int type, int protocol);

此时可以通过第三个参数protocol指定协议编号来告诉socket,你使用的是什么协议族中的哪个子协议。

什么是协议编号?
每个协议的唯一识别号,制订协议的人给的。

type 套接字类型

套接字类型,说白了就是进一步指定,你想要使用协议族中的那个子协议来通信。

比如,如果你想使用TCP协议来通信,
首先:将domain指定为PF_INET,表示使用的是IPV4的TCP/IP协议族
其次:对type进行相应的设置,进一步表示我想使用的是TCP/IP协议族中的TCP协议。

type的常见设置值:SOCK_STREAM、SOCK_DGRAM、SOCK_RDM、SOCK_NONBLOCK、SOCK_CLOEXEC。

  • SOCK_STREAM:
1
int socket(int domain, int type, int protocol);

将type指定为SOCK_STREAM时,表示想使用的是“有序的、面向连接的、双向通信的、可靠的字节流通信”,并且支持带外数据。

有关什么是带外数据我们后面进行说明。
stream 就是字节流的意思。

1
int socket(int domain, int type, int protocol);

如果domain被设置为PF_INET,type被设置为SOCK_STREAM,就表示你想使用TCP来通信,因为在TCP/IP协议族里面,只有TCP协议是“有序的、面向连接的、双向的、可靠的字节流通信”。

使用TCP协议通信时TCP并不是孤立的,它还需要网络层和链路层协议的支持才能工作。

如果type设置为SOCK_STREAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似TCP这样的可靠传输协议。

  • SOCK_DGRAM:
1
int socket(int domain, int type, int protocol);

将type指定为SOCK_DGRAM时,表示想使用的是“无连接、不可靠的、固定长度的数据报通信”。

固定长度意思是说,分组数据的大小是固定的,不管网络好不好,不会自动去调整分组数据的大小,所以“固定长度数据报”其实就是“固定长度分组数据”的意思。

当domain指定为PF_INET、type指定为SOCK_DGRAM时,就表示想使用的是TCP/IP协议族中的中的UDP协议,因为在TCP/IP协议族中,只有UDP是“无连接、不可靠的、固定长度的数据报通信”。

同样的UDP协议通信不可能独立工作,需要网络层和链路层协议的支持。

如果type设置为SOCK_DGRAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似UDP这样的不可靠传输协议。
后面说明UDP时就会使用SOCK_DGRAM。

  • SOCK_RDM:表示想使用的是“原始网络通信”。
1
int socket(int domain, int type, int protocol);

当domain指定为TCP/IP协议族、type指定为SOCK_RDM时,就表示想使用ip协议来通信,直接使用IP协议来通信,其实就是原始的网络通信。

我们在前面说明TCP/IP协议族的时候,应用程序可以直接越过TCP协议或者UDP协议,直接使用IP协议进行通信,也就是原始的网络通信。

TCP/IP协议族

为什么称为原始通信?
以TCP/IP协议族为例,TCP/IP之所以能够实现网络通信,最根本的原因是因为IP协议的存在,ip协议才是关键,只是原始的IP协议只能实现最基本的通信,还缺乏很多精细的功能,所以才多了基于IP工作的TCP协议和UDP协议这两种传输层协议,TCP/UDP弥补了IP缺失的精细功能。

尽管ip提供的只是非常原始的通信,但是我们确实可以使用比较原始的IP协议来进通信,特别是当你不满意TCP和UDP的表现,你想实现一个比TCP和UDP更好的传输层协议时,你就可以直接使用ip协议,然后由自己的应用程序来实现符合自己要求的类似tcp/udp协议,不过我们几乎遇不到这种情况,这里了解下即可。

如果type设置为SOCK_RDM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似ip这样最基本的原始通信协议。

  • SOCK_NONBLOCK:将socket返回的文件描述符指定为非阻塞的。
1
int socket(int domain, int type, int protocol);

如果不指定这个宏的话,使用socket返回“套接字文件描述符”时,不管是是用来“监听”还是用来通信,都是阻塞操作的,但是指定SOCK_NONBLOCK这个宏的话,就是非阻塞的。

当然也可以使用fcntl函数来指定SOCK_NONBLOCK,至于fcntl怎么用,我们在高级IO博客中已经有非常详细的说明。

SOCK_NONBLOCK宏可以和前面的宏进行 | 运算,比如:
SOCK_STREAM | SOCK_NONBLOCK
上面的宏表示使用面向连接的、可靠的字节流通信,同时socket返回的文件描述符非阻塞。

  • SOCK_CLOEXEC:表示一旦进程exec执行新程序后,自动关闭socket返回的“套接字文件描述符”。
1
int socket(int domain, int type, int protocol);

图解说明:

SOCK_CLOEXEC宏说明

再次说明,type指定 SOCK_CLOEXEC 宏,一旦进程通过调用exec函数去执行新程序,之前调用socket函数返回的套接字文件描述符将会自动关闭,在新程序里面无法使用上面的套接字文件描述符sockfd。

这个标志也是可以和前面的宏进行 | 运算的,不过一般不指定这个标志。
例如:SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC

1
int socket(int domain, int type, int protocol);
protocol 指定协议号

一般情况下protocol写0,表示使用domain和type所指定的协议。

不过如果domain和type所对应的协议有好几个时,此时就需要通过具体的协议号来区分了,否则写0即可,表示domain和type所对应的协议就一个,不需要指定协议号来区分。

一般情况下使用TCP的时候,domain和type 两个参数所指定的TCP协议就只有一个,
所以第三个参数设置为0即可。

在哪里可以查到协议号?
所有协议的协议号都被保存在了/etc/protocols下。
打开文件查看协议号:vi /etc/protocols
查看协议号

协议 编号
ip 对应 0
icmp 对应 1
igmp 对应 2
tcp 对应 6
udp 对应 17

如果现在要调用socket 函数使用tcp协议,那么通过下面方式指定参数:

1
socket(PF_INET, SOCK_STREAM, 0);

我们可以看到上面指定的TCP协议标号为6。
如果调用socket 函数为:

1
socket(PF_INET, SOCK_STREAM, 6);

也不会出现错误,相当于重复指定。通过前两个参数本来就可以指定TCP协议,结果又指定了TCP协议号。不过一般来说如果前面两个参数能够确定协议,那么就不指定协议编号,协议编号写0即可。因为指定协议号会更加麻烦,而且协议号也记不住。除非前面两个参数所对应的协议分成了好几个子协议,那么就必须通过协议编号进行区分。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>

void  print_err(char * str,int line,int err_no)//出错处理函数
{
    printf("%d,%s: %s\n",line,str,strerror(err_no));
    exit(-1);
}
int main(void)
{
    int sockfd = -1; //存放套接字文件描述符
    /*创建使用TCP协议通信的套接字文件*/
    sockfd =  socket(PF_INET,SOCK_STREAM,0); //指定TCP协议
    if(-1 == sockfd) //进行出错处理
        print_err("socket fail",__LINE__,errno);
    return 0;
}

运行结果为:
调用socket函数运行结果

代码运行没有问题。下篇博客进行第二步:调用bind函数。