Linux
内核net/socket.c
定义了一套socket
的操作api
。图1展示了socket
层所处与TCP/IP
协议栈之上和应用层之下。
socket()函数
socket
函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket
描述符(socket descriptor),它唯一标识一个socket
。这个socket
描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
int socket(int domain, int type, int protocol);
参数:
-
domain
: 即协议域,又称为协议族(family)。常用的协议组有:
- AF_INET(ipv4地址(32位的)与端口号(16位的)的组合)
- AF_INET6( IPv6 的地址族)
- AF_LOCAL(或称AF_UNIX,Unix域socket)(用一个绝对路径名作为地址)
- AF_ROUTE
-
type
: 指定socket类型。常用的socket类型有:
- SOCK_STREAM
- SOCK_DGRAM
- SOCK_RAW
- SOCK_PACKET
- SOCK_SEQPACKET等等
-
protocol
:指定协议。常用的协议有:
- IPPROTO_TCP(TCP传输协议)
- IPPTOTO_UDP(UDP传输协议)
- IPPROTO_SCTP(STCP传输协议)
- IPPROTO_TIPC等(TIPC传输协议)
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
bind()函数
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()
、listen()
时系统会自动随机分配一个端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 即socket
描述字,它是通过socket()
函数创建了,唯一标识一个socket
。bind()
函数就是将给这个描述字绑定一个名字。-
addr
: 一个const struct sockaddr *
指针,指向要绑定给sockfd
的协议地址。这个地址结构根据地址创建socket
时的地址协议族的不同而不同,如ipv4
对应的是:struct sockaddr_in { short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* Internet 地址 */ unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
注意,原来的
sockaddr
的格式是(16个字节):struct sockaddr { unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data[14]; /*14字节协议地址*/ };
用
sockaddr_in
这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和struct sockaddr
一样) 应该使用函数bzero()
或memset()
来全部置零。 同时,这一重要的字节,一个指向sockaddr_in
结构体的指针也可以被指向结构体sockaddr
并且代替它。这样的话即使socket()
想要的是struct sockaddr *
,你仍然可以使用struct sockaddr_in
,并且在最后转换。ipv6
对应的是:struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };
Unix
域对应的是:#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
addrlen
:地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
网络字节序与主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian
和Little-Endian
的定义如下:
a) Little-Endian
就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian
就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP
首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian
。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket
。
在网络编程中,能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表 示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。
这里有:
- htons()--"Host to Network Short"
- htonl()--"Host to Network Long"
- ntohs()--"Network to Host Short"
- ntohl()--"Network to Host Long"
为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?
答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。
处理IP地址
listenAddr.sin_addr.s_addr = INADDR_ANY;
// listenAddr.sin_addr.s_addr = inet_addr(IP);
// inet_pton(AF_INET,IP,&listenAddr.sin_addr);
listen()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket
int listen(int sockfd, int backlog);
参数:
-
sockfd
::即socket描述字,唯一的id。 -
backlog
:相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect()函数
客户端调用connect()发出连接请求,服务器端就会接收到这个请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
-
sockfd
:socket描述符。 -
addr
:服务器的socket地址。 -
addrlen
:socket地址的长度
再一次,你应该检查 connect() 的返回值--它在错误的时候返回-1,并 设置全局错误变量 errno。 同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。
客户端通过调用connect函数来建立与TCP服务器的连接。
accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
-
sockfd
:服务器的socket描述字。 -
addr
:指向struct sockaddr *
的指针,用于返回客户端的协议地址。 -
addrlen
:协议地址的长度。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
send() and recv()函数
两个函数用于流式套接字或者数据报套接字的通讯
send()函数
int send(int sockfd, const void *msg, int len, int flags);
参数:
-
sockfd
:想要发送数据的套接字描述符。 -
msg
:指向发送的消息数据的指针。 -
flags
:一般设置为0.
send() 返回实际发送的数据的字节数--它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是 发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也 有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。
recv() 函数
int recv(int sockfd, void *buf, int len, unsigned int flags);
参数:
-
sockfd
:要读的套接字描述符。 -
buf
:读取数据的缓冲区。 -
len
:缓冲区的最大长度。 -
flags
:一般设置为0.
recv()
返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。
sendto() 和 recvfrom()函数
既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!
sendto()函数
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。
recvfrom() 函数
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。
recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。
close()函数
关闭文件描述符。
close(sockfd);
shutdown()函数
它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用:
int shutdown(int sockfd, int how);
参数:
-
sockfd
:是你想要关闭的套接字文件描述符。 -
how
:值是下面的其中之 一:- 0 – 不允许接受
- 1 – 不允许发送
- 2 – 不允许发送和接受(和 close() 一样)
shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。
getpeername()函数
函数 getpeername()
告诉你在连接的流式套接字上谁在另外一边。函数是这样的:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
参数:
-
sockfd
:连接的流式套接字的描述符。 -
addr
:是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的信息。 -
addrlen
:是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)
函数在错误的时候返回 -1,设置相应的 errno。
一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号
gethostname()函数
它返回你程序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你的机器的 IP 地址。
#include <unistd.h>
int gethostname(char *hostname, size_t size);
参数:
-
hostname
:字符串指针,保存返回的主机名。 -
size
:hostname 数组的字节长度。
函数调用成功时返回 0,失败时返回 -1,并设置 errno。
简单的实例
服务器demo代码server.c
:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define SERVER_PORT 10086 /*定义用户连接端口*/
#define MAX_CONNECT_NUM 10 /*多少等待连接控制*/
const char *IP = "127.0.0.1"; /*设定本地地址*/
void ServerDemo()
{
int sockFd; //服务器的文件描述符
struct sockaddr_in serverAddr; // 服务器端的ip地址和端口即协议族
sockFd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sockFd == -1)
{
printf("create Server Socket error!\n");
exit(-1);
}
// 设置ip地址和协议族及端口
serverAddr.sin_family = AF_INET;
// serverAddr.sin_addr.s_addr = inet_addr(IP);
inet_pton(AF_INET, IP, &serverAddr.sin_addr);
serverAddr.sin_port = htons(SERVER_PORT);
bzero(&serverAddr.sin_zero, sizeof(serverAddr.sin_zero));
// 绑定,服务器需要绑定
int bind_ret = bind(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (bind_ret == -1)
{
printf("bind error!\n");
if (errno == EACCES)
{
exit(1); // 没有权限
}
else if (errno == EADDRINUSE)
{
exit(2); // 端口被占用
}
else
{
exit(errno);
}
}
// 监听
int ret = listen(sockFd, MAX_CONNECT_NUM);
if (ret == -1)
{
printf("listen error!\n");
exit(-1);
}
// do once
// 接收客户端的连接
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
int connectFd = accept(sockFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
if (connectFd == -1)
{
printf("connect error!\n");
printf("accept failed, errno:%d\n", errno);
}
else
{
// 接收一次消息
char clientIp[INET_ADDRSTRLEN];
printf("connected with %s:%d\n", inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN), ntohs(clientAddr.sin_port));
char data[1024];
recv(connectFd, data, sizeof(data), 0);
printf("receive data: %s\n", data);
}
close(sockFd); // 关闭套接字
}
int main()
{
ServerDemo();
return 0;
}
客户端demo代码client.c
:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define PORT 10086 /* 客户机连接远程主机的端口 */
#define MAXDATASIZE 100 /* 每次可以接收的最大字节 */
const char *IP = "127.0.0.1"; /* 设定连接IP地址为本地IP地址 */
void ClientDemo()
{
int sockFd;
// 创建客户端套接字
sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd == -1)
{
printf("create client socket error!\n");
exit(-1);
}
// 设定需要连接的服务器的IP地址、地址族以及端口
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
// serverAddr.sin_addr.s_addr = inet_addr(IP);
inet_pton(AF_INET, IP, &serverAddr.sin_addr);
serverAddr.sin_port = htons(PORT);
// 连接服务器
int ret = connect(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (ret == -1)
{
printf("connect failed, errno:%d\n", errno);
}
else
{
char data[] = "hello,world!"; // 定义缓冲区
send(sockFd, data, sizeof(data), 0); // 发送数据
}
close(sockFd); // 关闭客户端套接字
}
int main()
{
ClientDemo();
return 0;
}
分别执行下面的命令:
gcc server.c -o server
gcc client.c -o client
执行
<font color='pink'>下面是UDP通信的例子(UDP不存在服务器客户端的概念)</font>
udpListen.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define MYPORT 4950 /* the port users will be sending to */
#define MAXBUFLEN 100
const char *IP = "127.0.0.1";
void UDP_ListenDemo()
{
int udpSockFd;
struct sockaddr_in listenAddr; // 接收方地址
struct sockaddr_in talkAddr; // 发送方的地址
udpSockFd = socket(AF_INET, SOCK_DGRAM, 0);
if (udpSockFd == -1)
{
printf("create udp sock error!\n");
exit(-1);
}
listenAddr.sin_family = AF_INET;
listenAddr.sin_addr.s_addr = INADDR_ANY;
// listenAddr.sin_addr.s_addr = inet_addr(IP);
// inet_pton(AF_INET,IP,&listenAddr.sin_addr);
listenAddr.sin_port = htons(MYPORT);
bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));
if (bind(udpSockFd, (struct sockaddr *)&listenAddr, sizeof(listenAddr)) == -1)
{
printf("bind error!\n");
exit(-1);
}
socklen_t talkAddrLen = sizeof(talkAddr);
char buf[MAXBUFLEN];
int numbytes;
numbytes = recvfrom(udpSockFd, buf, MAXBUFLEN, 0, (struct sockaddr *)(&talkAddr), &talkAddrLen);
if (numbytes == -1
{
printf("revefrom error!\n");
exit(-1);
}
printf("got packet from %s\n", inet_ntoa(talkAddr.sin_addr)); // 得到对方的ip地址
printf("packet is %d bytes long\n", numbytes);
buf[numbytes] = '\0';
printf("packet contains \"%s\"\n", buf); //输出接收的内容
close(udpSockFd); // 关闭UDP套接字
}
int main()
{
UDP_ListenDemo();
return 0;
}
udpTalk.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define MYPORT 4950
const char *IP = "127.0.0.1";
void UDP_TalkDemo()
{
int udpTalkFd;
struct sockaddr_in listenAddr;
struct hostent *he;
int numbytes;
udpTalkFd = socket(AF_INET, SOCK_DGRAM, 0);
if (udpTalkFd == -1)
{
fprintf(stdout, "create udp sock error!\n");
exit(-1);
}
listenAddr.sin_family = AF_INET;
listenAddr.sin_addr.s_addr = inet_addr(IP);
// inet_pton(AF_INET,IP,&listenAddr.sin_addr);
listenAddr.sin_port = htons(MYPORT);
bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));
char buf[] = "Hello!";
numbytes = sendto(udpTalkFd, buf, sizeof(buf), 0, (struct sockaddr *)&listenAddr, sizeof(listenAddr));
if (numbytes == -1)
{
printf("udp send error!\n");
exit(-1);
}
printf("sent %d bytes to %s\n", numbytes, inet_ntoa(listenAddr.sin_addr));
close(udpTalkFd);
}
int main()
{
UDP_TalkDemo();
return 0;
}
编写Makefile进行编译
test: udpListen.c
gcc udpListen.c -o udpListen
gcc udpTalk.c -o udpTalk
分别执行:
make test
./udpListen
./udpTalk
结果:
参考: