大端与小端的区别;
什么是字节序;
为什么出现网络字节序。
1、字节序
字节序就是说一个对象的多个字节在内存中如何排序存放的;比如我们要想往一个地址a中写入一个整型数据0x12345678,那么最后在内存中是如何存放这4个字节的呢?
0x12这个字节值为最高有效字节,也就是整数值的最高位,0x78为最低有效字节。
大端字节序:高位地地址,节省空间;最高有效字节落在低地址上
小端字节序:最低有效字节落在低地址上的字节存放方式;
同样的字节序12 34 56 78在大端序机器中会识别为0x12345678;
小端序机器中识别为:0x78563421(0x12+0x3400+0x560000…=0x
78563421)。
Intel处理器大多数使用小端字节序;Motorola处理器大多数使用大端(big endian)字节序;arm即可设为大端,又可设为小端。如果直接在2个不同字节序的主机之间传输数据会引起混乱。
2、比特序
字节序是一个对象中的多个字节之间的顺序问题,比特序就是一个字节中的8个比特位(bit)之间的顺序问题;一般情况下系统的比特序和字节序一致的;
字节序是一个对象中的多个之间的顺序问题,比特序就是一个字节中的8个比特位(bit)之间的顺序问题;一般情况下系统的比特序和字节序保持一致的;一个字节由8个bit组成,这8个bit也存在如何排序的情况,跟字节序类似的有最高有效比特位、最低有效比特位。
比特序1 0 0 1 0 0 1 0在大端系统中最高有效比特位为1,最低有效比特位为0,字节的值为0x92;在小端系统中最高、最低有效比特序则相反为0、1,字节的值为0x49。
3、字节序转换函数ntohl(s)、htonl(s)
在socket编程中经常要用到网络字节序转换函数ntohl、htonl来进行主机序和网络序(大端序)的转换,在主机序为小端的系统中字节序列78 56 34 12经过htonl转换后字节序列变成12 34 56 78:
4、网络字节序
为了能让不同处理器架构的机器进行通信,他们都需要将本机上的字节序转换成网络字节序,这样就解决了不同处理器之间的矛盾。一般来说网络字节序和大端机器上的字节序是一样的;POSIX提供了4个函数(也可能是用宏来实现的),可以让本机字节序和网络字节序之间进行互转。
它们分别是:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//32位无符号数主机序转换网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);//32位无符号数网络序转换主机序
uint16_t ntohs(uint16_t netshort);
其中函数名字中h表示host(本机),n表示network(网络),而 l 表示要转换的数据是4字节,s 表示要转换的数据是2字节。
#include <stdio.h>
#include <arpa/inet.h>
union INT
{
char data[4];
int x;
};
int main()
{
int i;
union INT a;
union INT b;
a.x = 0x10203040;
b.x = htonl(a.x);//本机字节序转换为网络字节序
printf ("本机字节序小端 a = 0x%08x\n",a.x);//08指定数据的最小输出位数为8
//,若不够8位,则补零,若大于8位,则按照原位数输出
for (i = 0; i < 4; ++i)
{
printf ("[%p] : %02x\n",a.data+i,a.data[i]);
}
putchar('\n');
printf ("网络字节序一般大端: b = 0x%08x\n",b.x);
for (i = 0; i < 4; ++i)
{
printf ("[%p] : %02x\n",b.data+i,b.data[i]);
}
return 0;
}
本机字节序是小端的,网络字节序是大端需要通过函数htonl把本机地址转换成大端的。
IP地址
IPv4地址本质上是32位无符号整数;在Linux操作系统中,使用了in_addr_t类型来表示这样一个4字节的整数,它是由typedef定义的:
typedef unit32_t in_addr_t;
in_addr_t这个类型保存的数据,到底是按本机字节序保存的,还是网络字节序保存的,这是不确定的。因此Linux重新定义一个结构体来保存IP地址(网络字节序)
struct in_addr
{
in_addr_t s_addr;
}
如果说你传了一个本机字节序的 unsigned int 类型的整数给 in_addr 的 s_addr 成员,很抱歉,后面使用到该结构体的函数都会出错。
IP地址转换相关的函数
aton中a(address)表示点分十进制的地址,n表示网络字节序地址,ntoa类似;
函数lnaof表示local network address part of the Internet address in,即主机号;
函数netof表示network number part of the Internet address in,即网络号;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 将点分十进制直接转换成 in_addr 类型,我们很喜欢这个函数
int inet_aton(const char *cp, struct in_addr *inp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是网络字节序
in_addr_t inet_addr(const char *cp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是本机字节序
in_addr_t inet_network(const char *cp);
// 将 in_addr 地址转换成点分十进制,注意这个函数不是线程安全的
char *inet_ntoa(struct in_addr in);
// 根据网络号和主机号制作 in_addr 类型地址
struct in_addr inet_makeaddr(int net, int host);
// 返回 ip 地址的主机号,返回的结果是本机字节序的
in_addr_t inet_lnaof(struct in_addr in);
// 返回 ip 地址的网络号,返回的结果是本机字节序的
in_addr_t inet_netof(struct in_addr in);
网络地址
#if 0
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
// 将 cp 字符串表示的 IP 地址,转换为网络字节序的 IP 地址
// cp 可以是以下形式:
// a.b.c.d 如:192.168.0.105
// a.b.c c 占 16 位
// a.b b 占 24 位
// a a 占 32 位
// 成功返回 1,失败返回 0
in_addr_t inet_addr(const char *cp);
// 将 cp 字符串表示的 IP 地址,转换为网络字节序的二进制形式
// 成功返回 IP 的网络字节序,失败返回 INADDR_NONE
in_addr_t inet_network(const char *cp);
// 将 cp 字符串表示的 IP 地址,转换为本机字节序的二进制形式
// 成功返回 IP 的本机字节序,失败返回 -1
char *inet_ntoa(struct in_addr in);
// 将 in 表示的网络字节序的 IP 地址转换为“点分十进制”形式的字符串
// 成功返回 IP 地址的字节串,失败返回 "255.255.255.255"
// 注意,返回的地址是一个静态局部缓冲区,后续的调用会覆盖之前的结果
// IPv4 地址
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
typedef uint32_t in_addr_t;
#endif
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
char *ip;
struct in_addr addr;
in_addr_t inaddr;
if (argc != 2)
{
printf ("用法: %s <IP地址>\n",ip);
return -1;
}
ip = argv[1];
if (0 == inet_aton(ip,&addr)) //把有效的IP地址转换为网络字节序
{
printf ("%s 是无效的IP地址\n",ip);
}
else
{
printf ("%s 对应的网络字节序数值为: %#x\n",ip,addr.s_addr);
}
inaddr = inet_addr(ip); //把有效的IP地址转换为网络字节序
if (INADDR_NONE == inaddr) //INADDR_NONE表示禁止的IP地址
{
printf ("%s 是非法IP地址\n",ip);
}
else
{
printf ("%s 对应的网络字节序数值为: %#x\n",ip,inaddr);
}
inaddr = inet_network(ip); //把有效的IP地址转换为本机字节序
if (-1 == inaddr)
{
printf("%s 是非法的IP地址\n",ip);
}
else
{
printf ("%s 对应的本机字节序数值为: %#x\n",ip,inaddr);
}
ip = inet_ntoa(addr); //把网络字节序转换为IP地址(点分十进制)
printf ("%#x 转换为IP地址: %s\n",addr.s_addr,ip);
return 0;
}
解析域名
#if 0
头文件:#include <netdb.h>
#include <sys/socket.h>
extern int h_errno;
函数原型:struct hostent *gethostbyname(const char *name);
// 将 name 转换为 struct hostent 指针指向的主机信息
// 成功返回非空指针(一个hostent的结构),失败返回 NULL
// 非空时,指针可能指向静态缓冲区
头文件:#include <sys/socket.h> /* for AF_INET */
函数原型:struct hostent *gethostbyaddr(const void *addr,socklen_t len, int type);
//函数gethostbyaddr取一个二进制的IP地址并试图找到相应于此地址的主机名与gethostbynme相反
void sethostent(int stayopen);
void endhostent(void);
void herror(const char *s);
// 将 s 后加 ": " 再加 h_errno 表示的错误信息字符串一起输出
const char *hstrerror(int err);
// 将 err 表示的错误信息转换为字符串
/* System V/POSIX extension */
struct hostent *gethostent(void);
/* GNU extensions */
struct hostent *gethostbyname2(const char *name, int af);
int gethostent_r(
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
int gethostbyaddr_r(const void *addr, socklen_t len, int type,
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
int gethostbyname_r(const char *name,
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
int gethostbyname2_r(const char *name, int af,
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
// h_addr_list 数组并不是字符串指针数组,而是地址的网络字节序的
// 二进制数据的指针数组
#endif
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
extern int h_errno;
int main(int argc,char *argv[])
{
struct hostent *hostent;
char *hostname;
if (argc != 2)
{
printf ("用法: %s <域名>\n",argv[0]);
return -1;
}
hostname = argv[1];
hostent = gethostbyname(hostname); //用域名或主机名获取IP地址
if (NULL == hostent)
{
herror("gethostbyname()失败");
printf ("gethostbyname()失败:%s\n",hstrerror(h_errno));
return -1;
}
//解析主机信息
printf ("主机官方名称: %s\n",hostent->h_name); //主机的规范名
for (int i = 0; hostent->h_aliases[i];i++)
{
printf ("[%d] - %s\n",i,hostent->h_aliases[i]); //主机的别名
}
if (hostent->h_addrtype == AF_INET) //主机IP地址的类型
{
printf ("地址类型是 AF_INET\n");
}
else
{
printf ("地址类型不是 AF_INET\n");
}
printf ("地址长度:%d\n",hostent->h_length); //主机的IP地址长度
for (int i = 0; hostent->h_addr_list[i];i++) //获取主机的IP地址
{
char *ip = inet_ntoa(*((struct in_addr *)hostent->h_addr_list[i]));
printf ("[%d]:%s\n0",i,ip);
}
char *ip = inet_ntoa(*((struct in_addr *)hostent->h_addr));
printf ("[0]:%s\n",ip);
return 0;
}
一、套接字socket
安装socket直接使用socket函数即可
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
【1】应用头文件: #include <sys/socket.h>
【2】函数标准式:int socket(int domain, int type, int protocol);
【3】功能与作用:创建通信的端口
【4】函数参数 :domain告诉操作系统要用哪家写的网络协议,比如AF_INET(IPv4协议),AF_INET6(IPv6协议);
type字段表示通信的交流方式,有以下选项:
SOCK_STREAM:有序的,可靠的,双向的基于连接的字节流,
SOCK_DGRAM:无连接的,不可靠的,固定长度的报文,
SOCK_RAM:原始套接字。
protocol:用哪家的网络协议确定了,用哪种通信方式确定了,最后使用哪种具体的协议来实现这种通信方式;通常设置为0,表示只用一种默认的方式。SOCK_STREAM默认使用的协议是TCP,SOCK_DGRAM默认使用的协议是UDP。
【5】返回值 :成功返回套接字的描述符,失败返回-1;
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
int main()
{
int sockfd,sockfd1,sockfd2;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if (-1 == sockfd)
{
printf ("创建套接字失败");
return -1;
}
printf ("创建套接字成功,套接字为 %d \n",sockfd);
sockfd1 = socket(AF_INET,SOCK_STREAM,0);
if (-1 == sockfd1)
{
printf ("创建套接字失败");
return -1;
}
printf ("创建套接字成功,套接字为 %d \n",sockfd1);
/*sockfd2 = socket(AF_INET,SOCK_RAM,IPPROTO_TCP); //原始套接字比较特殊
if (-1 == sockfd2)
{
printf ("创建套接字失败");
return -1;
}
printf ("创建套接字成功,套接字为 %d \n",sockfd2);*/
return 0;
}
二、将socket与套接字地址绑定
在网络编程中,把(IP地址+端口号)这样的一对值称呼为套接字地址;在计算机中,通常使用192.168.166.5:80 这种ip:port 的形式来表示套接字地址。
socket地址有多种:sockaddr是通用地址结构;sockaddr_in是IPv4地址结构;sockaddr_un是Unix地址结构。
套接字地址在程序中是一个结构体
通用地址结构体:
struct sockaddr
{
unsigned short sa_family; /* 地址族, AF_xxx /
char sa_data[14]; / 14 字节的协议地址 */
};
IPv4结构体:
struct sockaddr_in {
sa_family_t sin_family; /* 这个值固定写 AF_INET /
in_port_t sin_port; / 网络字节序的端口号 /
struct in_addr sin_addr; / in_addr 类型的 ip 地址 */
};
函数bind可以将套接字地址与socket进行绑定
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这上面的套接字地址的类型是 struct sockaddr 而不是我们前面学过的 struct sockaddr_in 呢?你的疑问是正常的,因为 bind 函数不仅仅是设计给 IPv4 用的,也会给 IPv6 地址,所以 struct sockaddr 就有点像一个抽象类。
因此,你在构造完套接字地址后,直接把 struct sockaddr_in 的地址强转成 struct sockaddr 指针就行了。
参数 addrlen 是参数 addr 指向的套接字地址的结构体大小
还需要注意的地方:如果你并不想让别的进程连接到你的进程,你并不需要将 socket 绑定到套接字地址上。需要绑定套接字地址的进程,通常就是可以让别人找到你并连接上你,所以这种进程通常都称之为服务器进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char **argv)
{
int sockfd; //套接字描述符变量
struct sockaddr_in svr_addr,clt_addr; //服务器地址与客户端地址变量
in_port_t port; //服务器端口
in_addr_t inaddr; //网络字节序保存
char *ip ;
socklen_t socklen = sizeof svr_addr;
char recv_buf[32] = "";
char send_buf[32] = "hello server";
if (argc != 3)
{
printf ("用法:%s <本机IP> <服务器端口号>\n",argv[0]);
return -1;
}
ip = argv[1];
port = atoi(argv[2]);
sockfd = socket(AF_INET,SOCK_DGRAM,0); //创建套接字失败返回-1
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
inaddr = inet_addr(ip); //本机地址转换为网络字节序的二进制形式
if (INADDR_NONE == inaddr)
{
printf ("%s 是非法IP地址\n",ip);
goto _out;
}
svr_addr.sin_family = AF_INET; //设置IPv4类型
svr_addr.sin_port = htons(port); //设置网络端口号
svr_addr.sin_addr.s_addr = inaddr; //IP地址
if (-1 == sendto(sockfd,send_buf,strlen(send_buf),0,
(struct sockaddr *)&svr_addr,socklen))
{
perror("发送失败");
goto _out;
}
printf ("发送成功\n");
if (-1 == recvfrom(sockfd,recv_buf,strlen(recv_buf),0,
(struct sockaddr *)&clt_addr,&socklen))
{
perror("接收失败");
goto _out;
}
printf ("收到 %s(%d)发来的消息:%s\n",
inet_ntoa(clt_addr.sin_addr),
ntohs(clt_addr.sin_port),recv_buf);
_out:
close(sockfd); //关闭套接字
return 0;
}
UDP编程流程:
服务器
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 创建用于通信的端口,返回描述符
// domain 包含以下域:
// Name Purpose Man page
// AF_UNIX, AF_LOCAL Local communication unix(7)
// AF_INET IPv4 Internet protocols ip(7)
// AF_INET6 IPv6 Internet protocols ipv6(7)
// AF_IPX IPX - Novell protocols
// AF_NETLINK Kernel user interface device netlink(7)
// AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
// AF_AX25 Amateur radio AX.25 protocol
// AF_ATMPVC Access to raw ATM PVCs
// AF_APPLETALK AppleTalk ddp(7)
// AF_PACKET Low level packet interface packet(7)
// AF_ALG Interface to kernel crypto API
// type 有以下选项:
// SOCK_STREAM 基于流的,适用于 TCP 套接字
// SOCK_DGRAM 基于数据报的,适用于 UDP 套接字
// SOCK_RAW 原始套接字
// protocol 子协议,如果没有子协议则为 0
// 成功返回套接字的描述符,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 绑定长度为 addrlen 的 addr 地址到 sockfd 套接字
// sockfd 套接字描述符
// addr 通用地址指针,传递具体地址时要进行强制类型转换
// addrlen 地址长度
// 成功返回 0,失败返回 -1
// 通用地址结构体
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 只能用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// flags 可取 0 或以下值按位或:
// MSG_DONTWAIT 非阻塞
// MSG_OOB 带外数据(紧急数据)
// MSG_PEEK 只提取数据而不从接收队列中删除它
// MSG_WAITALL 阻塞直到所有请求都满足
// recv() 等价于以下调用:
// recvfrom(fd, buf, len, flags, NULL, 0));
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 用于 UDP,但也可以用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// 保存发送者的地址信息到大小为 addrlen 的地址 src_addr 中
// 注意,尽管 addrlen 是用来存放发送者的地址的大小的,但在有的系统
// 中必须提供一个正确的大小,才能正确接收网络数据
// 以上函数成功时返回收到的字节数,失败时返回 -1
// 返回 0 的情况:
// 1. 基于流的套接字(TCP),发送方已关闭,接收方会收到 0 个字节,
// 这相当于"文件尾"
// 2. 基于数据报的套接字(UDP)允许长度为 0 的数据报,当收到这样的
// 数据报,返回 0 个字节
// 3. 从流套接字收到 0 个字节时返回 0 字节
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 只用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送
// flags 取值为 0 或以参下参数按位或:
// MSG_DONTWAIT 非阻塞
// MSG_NOSIGNAL 向已关闭的流套接字写不产生 SIGPIPE 信号
// MSG_OOB 带外数据(紧急数据)
// send() 等价于以下调用:
// sendto(sockfd, buf, len, flags, NULL, 0);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 既可以用于 UDP 也可以用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送到大小为 addrlen
// 的地址 dest_addr
// send() 和 sendto() 返回成功发送的字节数,或失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
功能说明:
获取或者设置与某个套接字关联的选 项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该 将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议 号TCP。
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
// 获取/设置套接字选项
// level 可取以下值:
// SOL_SOCKET 基本套接口
// IPPROTO_IP IPv4套接口
// IPPROTO_IPV6 IPv6套接口
// IPPROTO_TCP TCP套接口
// optname 可取以下值:
// SO_BROADCAST 广播
// SO_RCVBUF 接收缓冲区
// SO_REUSEADDR 地址可重用
// SO_REUSEPORT 端口可重用
// SO_SNDBUF 发送缓冲区
// 成功返回 0,失败返回 -1,errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
#endif
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in svr_addr, clt_addr;
in_port_t port;
in_addr_t inaddr;
char *ip = "192.168.177.151";
int optval = 1; // 非 0 使能地址可重用
socklen_t socklen = sizeof clt_addr, optlen = sizeof optval;
char recv_buf[32] = "";
char send_buf[32] = "Hello client";
if (argc != 3) // 检查参数个数
{
printf("用法:%s <本机 IP> <端口号>\n", argv[0]);
return -1;
}
ip = argv[1];
port = atoi(argv[2]);
// 创建 UDP 套接字
sockfd = socket(AF_INET /*PF_INET*/, SOCK_DGRAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 设置套接字选项 - 地址可重用
if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
&optval, optlen))
{
perror("设置地址可重用失败");
goto _out;
}
// 绑定本机地址
inaddr = inet_addr(ip);
if (INADDR_NONE == inaddr)
{
printf("%s 是非法 IP 地址\n", ip);
goto _out;
}
svr_addr.sin_family = AF_INET; // PF_INET
svr_addr.sin_port = htons(port); // 端口必须转换为网络字节序
//svr_addr.sin_addr.s_addr = inaddr; // 绑定到本机的某一个 IP
// 绑定到本机任意网络接口
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr, sizeof svr_addr))
{
perror("绑定失败");
goto _out;
}
// 收
if (-1 == recvfrom(sockfd, recv_buf, sizeof recv_buf, 0,
(struct sockaddr *) &clt_addr, &socklen))
{
perror("接收失败");
goto _out;
}
printf("收到 %s(%d) 发来的消息:%s\n",
inet_ntoa(clt_addr.sin_addr),
ntohs(clt_addr.sin_port), recv_buf);
// 发
if (-1 == sendto(sockfd, send_buf, strlen(send_buf), 0,
(struct sockaddr *) &clt_addr, socklen))
{
perror("发送失败");
goto _out;
}
printf("发送成功\n");
_out:
// 关闭套接字
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 创建用于通信的端口,返回描述符
// domain 包含以下域:
// Name Purpose Man page
// AF_UNIX, AF_LOCAL Local communication unix(7)
// AF_INET IPv4 Internet protocols ip(7)
// AF_INET6 IPv6 Internet protocols ipv6(7)
// AF_IPX IPX - Novell protocols
// AF_NETLINK Kernel user interface device netlink(7)
// AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
// AF_AX25 Amateur radio AX.25 protocol
// AF_ATMPVC Access to raw ATM PVCs
// AF_APPLETALK AppleTalk ddp(7)
// AF_PACKET Low level packet interface packet(7)
// AF_ALG Interface to kernel crypto API
// type 有以下选项:
// SOCK_STREAM 基于流的,适用于 TCP 套接字
// SOCK_DGRAM 基于数据报的,适用于 UDP 套接字
// SOCK_RAW 原始套接字
// protocol 子协议,如果没有子协议则为 0
// 成功返回套接字的描述符,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 绑定长度为 addrlen 的 addr 地址到 sockfd 套接字
// sockfd 套接字描述符
// addr 通用地址指针,传递具体地址时要进行强制类型转换
// addrlen 地址长度
// 成功返回 0,失败返回 -1
// 通用地址结构体
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 只能用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// flags 可取 0 或以下值按位或:
// MSG_DONTWAIT 非阻塞
// MSG_OOB 带外数据(紧急数据)
// MSG_PEEK 只提取数据而不从接收队列中删除它
// MSG_WAITALL 阻塞直到所有请求都满足
// recv() 等价于以下调用:
// recvfrom(fd, buf, len, flags, NULL, 0));
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 用于 UDP,但也可以用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// 保存发送者的地址信息到大小为 addrlen 的地址 src_addr 中
// 注意,尽管 addrlen 是用来存放发送者的地址的大小的,但在有的系统
// 中必须提供一个正确的大小,才能正确接收网络数据
// 以上函数成功时返回收到的字节数,失败时返回 -1
// 返回 0 的情况:
// 1. 基于流的套接字(TCP),发送方已关闭,接收方会收到 0 个字节,
// 这相当于"文件尾"
// 2. 基于数据报的套接字(UDP)允许长度为 0 的数据报,当收到这样的
// 数据报,返回 0 个字节
// 3. 从流套接字收到 0 个字节时返回 0 字节
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 只用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送
// flags 取值为 0 或以参下参数按位或:
// MSG_DONTWAIT 非阻塞
// MSG_NOSIGNAL 向已关闭的流套接字写不产生 SIGPIPE 信号
// MSG_OOB 带外数据(紧急数据)
// send() 等价于以下调用:
// sendto(sockfd, buf, len, flags, NULL, 0);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 既可以用于 UDP 也可以用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送到大小为 addrlen
// 的地址 dest_addr
// send() 和 sendto() 返回成功发送的字节数,或失败返回 -1
#endif
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in svr_addr, clt_addr;
in_port_t port;
in_addr_t inaddr;
socklen_t socklen = sizeof svr_addr;
char *ip;
char recv_buf[32] = "";
char send_buf[32] = "Hello server";
// 检查参数个数
if (argc != 3)
{
printf("用法:%s <服务器 IP> <服务器端口号>\n", argv[0]);
return -1;
}
ip = argv[1];
port = atoi(argv[2]);
// 创建 UDP 套接字
sockfd = socket(AF_INET /*PF_INET*/, SOCK_DGRAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 客户端不必绑定本机地址
// 配置服务器地址
inaddr = inet_addr(ip);
if (INADDR_NONE == inaddr)
{
printf("%s 是非法 IP\n", ip);
goto _out;
}
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(port);
svr_addr.sin_addr.s_addr = inaddr;
// 发
if (-1 == sendto(sockfd, send_buf, strlen(send_buf), 0,
(struct sockaddr *) &svr_addr, socklen))
{
perror("发送失败");
goto _out;
}
printf("发送成功\n");
// 收
if (-1 == recvfrom(sockfd, recv_buf, sizeof recv_buf, 0,
(struct sockaddr *) &clt_addr, &socklen))
{
perror("接收失败");
goto _out;
}
printf("收到 %s(%d) 发来的消息:%s\n",
inet_ntoa(clt_addr.sin_addr),
ntohs(clt_addr.sin_port), recv_buf);
_out:
// 关闭套接字
close(sockfd);
return 0;
}
TCP编程流程:
服务器:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr 保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1
#endif
int main(int argc, char **argv)
{
in_port_t port;
int sockfd, connfd;
int optval = 1;
struct sockaddr_in svr_addr; // 服务器地址
int backlog = 5;
struct sockaddr_in clt_addr; // 客户端地址
socklen_t addrlen = sizeof clt_addr;
char rbuf[32] = "", wbuf[32] = "Hello client";
if (argc != 3)
{
printf("用法:%s <端口号> <监听队列长度>\n", argv[0]);
return -1;
}
port = atoi(argv[1]);
backlog = atoi(argv[2]);
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 设置地址可重用
if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof optval))
{
perror("设置地址可重用失败");
goto _out;
}
// 绑定服务器地址
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(port);
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//svr_addr.sin_addr.s_addr = htonl(inet_addr("192.168.0.121"));
if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr,
sizeof svr_addr))
{
perror("绑定失败");
goto _out;
}
// 设置监听队列长度
if (-1 == listen(sockfd, backlog))
{
perror("设置监听队列失败");
goto _out;
}
// 接收 5 次客户端连接
for (int i = 0; i < 5; i++)
{
// 接收客户端连接
connfd = accept(sockfd, (struct sockaddr *) &clt_addr, &addrlen);
if (-1 == connfd)
{
perror("接收客户端连接失败");
goto _out;
}
printf("第 %d 个客户端 %s(%d) 连接成功\n", i + 1,
inet_ntoa(clt_addr.sin_addr),
ntohs(clt_addr.sin_port));
// 收
if (-1 == read(connfd, rbuf, sizeof rbuf))
{
perror("接收失败");
goto _out2;
}
printf("收到客户端信息:%s\n", rbuf);
// 发
if (-1 == write(connfd, wbuf, strlen(wbuf)))
{
perror("发送失败");
goto _out2;
}
printf("发送成功\n");
_out2:
// 关闭连接
close(connfd);
}
_out:
// 关闭套接字
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr 保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1
#endif
int main(int argc, char **argv)
{
in_port_t port;
int sockfd;
struct sockaddr_in svr_addr; // 服务器地址
char rbuf[32] = "", wbuf[32] = "Hello server";
char *ip;
in_addr_t inaddr;
if (argc != 3)
{
printf("用法:%s <服务器 IP> <端口号>\n", argv[0]);
return -1;
}
ip = argv[1];
port = atoi(argv[2]);
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 发起向服务器的连接
inaddr = inet_addr(ip);
if (INADDR_NONE == inaddr)
{
printf("%s 是非法地址\n", ip);
goto _out;
}
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(port);
svr_addr.sin_addr.s_addr = inaddr;
if (-1 == connect(sockfd, (struct sockaddr *) &svr_addr,
sizeof svr_addr))
{
perror("连接服务器失败");
goto _out;
}
printf("连接服务器成功\n");
// 发
if (-1 == write(sockfd, wbuf, strlen(wbuf)))
{
perror("发送失败");
goto _out;
}
printf("发送成功\n");
// 收
if (-1 == read(sockfd, rbuf, sizeof rbuf))
{
perror("接收失败");
goto _out;
}
printf("收到服务器信息:%s\n", rbuf);
_out:
// 关闭套接字
close(sockfd);
return 0;
}
设置忽略SIGPIPE信号
服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr 保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1
#endif
void handle_sigpipe(int signum)
{
printf("收到 SIGPIPE 信号\n");
}
int main(int argc, char **argv)
{
in_port_t port;
int sockfd, connfd;
int optval = 1;
struct sockaddr_in svr_addr; // 服务器地址
int backlog = 5;
struct sockaddr_in clt_addr; // 客户端地址
socklen_t addrlen = sizeof clt_addr;
char rbuf[32] = "", wbuf[32] = "Hello client";
if (argc != 3)
{
printf("用法:%s <端口号> <监听队列长度>\n", argv[0]);
return -1;
}
port = atoi(argv[1]);
backlog = atoi(argv[2]);
// 避免 SIGPIPE 信号
//signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
signal(SIGPIPE, handle_sigpipe); // 处理 SIGPIPE 信号
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 设置地址可重用
if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof optval))
{
perror("设置地址可重用失败");
goto _out;
}
// 绑定服务器地址
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(port);
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//svr_addr.sin_addr.s_addr = htonl(inet_addr("192.168.0.121"));
if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr,
sizeof svr_addr))
{
perror("绑定失败");
goto _out;
}
// 设置监听队列长度
if (-1 == listen(sockfd, backlog))
{
perror("设置监听队列失败");
goto _out;
}
// 接收 5 次客户端连接
for (int i = 0; i < 5; i++)
{
// 接收客户端连接
connfd = accept(sockfd, (struct sockaddr *) &clt_addr, &addrlen);
if (-1 == connfd)
{
perror("接收客户端连接失败");
goto _out;
}
printf("第 %d 个客户端 %s(%d) 连接成功\n", i + 1,
inet_ntoa(clt_addr.sin_addr),
ntohs(clt_addr.sin_port));
// 收
if (-1 == read(connfd, rbuf, sizeof rbuf))
{
perror("接收失败");
goto _out2;
}
printf("收到客户端信息:%s\n", rbuf);
for (int j = 0; j < 10; j++)
{
sleep(1);
// 发
if (-1 == write(connfd, wbuf, strlen(wbuf)))
{
perror("发送失败");
if (EPIPE == errno)
{
printf("客户端已关闭\n");
}
goto _out2;
}
printf("发送成功\n");
}
_out2:
// 关闭连接
close(connfd);
}
_out:
// 关闭套接字
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#if 0
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr 保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1
#endif
int main(int argc, char **argv)
{
in_port_t port;
int sockfd;
struct sockaddr_in svr_addr; // 服务器地址
char rbuf[32] = "", wbuf[32] = "Hello server";
char *ip;
in_addr_t inaddr;
if (argc != 3)
{
printf("用法:%s <服务器 IP> <端口号>\n", argv[0]);
return -1;
}
ip = argv[1];
port = atoi(argv[2]);
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("创建套接字失败");
return -1;
}
// 发起向服务器的连接
inaddr = inet_addr(ip);
if (INADDR_NONE == inaddr)
{
printf("%s 是非法地址\n", ip);
goto _out;
}
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(port);
svr_addr.sin_addr.s_addr = inaddr;
if (-1 == connect(sockfd, (struct sockaddr *) &svr_addr,
sizeof svr_addr))
{
perror("连接服务器失败");
goto _out;
}
printf("连接服务器成功\n");
// 发
if (-1 == write(sockfd, wbuf, strlen(wbuf)))
{
perror("发送失败");
goto _out;
}
printf("发送成功\n");
/*
// 收
if (-1 == read(sockfd, rbuf, sizeof rbuf))
{
perror("接收失败");
goto _out;
}
printf("收到服务器信息:%s\n", rbuf);
*/
_out:
// 关闭套接字
close(sockfd);
return 0;
}
对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:signal(SIGPIPE, SIG_IGN);
这样, 第二次调用write方法时, 会返回-1, 同时errno置为EPIPE. 程序便能知道对端已经关闭.
在linux下写socket的程序的时候,如果尝试send到一个disconnected socket上,就会让底层抛出一个SIGPIPE信号。
这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。因此我们需要重载这个信号的处理方法。调用以下代码,即可安全的屏蔽SIGPIPE:signal (SIGPIPE, SIG_IGN);
client端通过pipe发送信号到server端后,就关闭client端,这时server端返回信息给client端时就会产生Broken pipe信号了,服务其就会被系统结束。