https://www.cnblogs.com/jiangzhaowei/p/8261174.html
https://www.cnblogs.com/benxintuzi/p/4589819.html
https://zhuanlan.zhihu.com/p/441197320
网络层的 ip地址:可以唯一标识网络中的主机;
传输层的协议+端口:可以唯一标识主机中的应用程序;
三元组(ip地址,协议,端口)可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互;
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口;
Linux支持多种socket类型,即应用程序希望的通信服务类型:
sockfd
代表本次socket连接的文件句柄,后续的通信我们只需要像对待普通文件一样往这个文件句柄中读写数据即可实现socket通信的过程。
一个整数:
0 标准输入 stdin
1 标准输出 stdout
2 标准错误输出 stderr
对于服务器,其通信流程一般有如下步骤:
socket
函数创建socket,这一步会创建一个文件描述符fd;bind
函数将socket(也就是fd)绑定到某个ip和端口的二元组上;listen
函数开启侦听端口;accept
阻塞等待接受连接,当有客户端请求连接上来后,产生一个新的socket(客户端socket);send
或recv
函数开始与客户端进行数据通信;close
函数关闭客户端socket;对于客户端,其通信流程一般有如下步骤:
调用socket
函数创建客户端socket;
调用connect
函数尝试连接服务器;
连接成功以后调用send
或recv
函数开始与服务器进行数据通信;
通信结束后,调用close
函数关闭socket;
struct sockaddr_in
网络地址结构体
struct sockaddr_in {
uint8_t sin_len /*端口号长度(8位)*/
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* ip address */
char sin_zero[8] /*为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节*/
};
/* Internet address */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
e.g.
struct sockaddr_in server;
memset(&server,0,sizeof(struct sockaddr_in));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
// server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(port);
// INADDR_ANY就是inet_addr("0.0.0.0") 让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求。
// 为了简化编程一般将IP地址设置为INADDR_ANY
// 如果需要使用特定的IP地址则需要使用 inet_addr 和 inet_ntoa 完成 字符串 和 in_addr结构体 的互换
struct servant
存储服务相关的信息
struct servent {
char *s_name; // 服务名
char **s_aliases; // 服务别名列表
int s_port; // 端口号
char *s_proto; // 使用的协议
};
socket()
#include
#include
int socket(int domain, int type, int protocol);
/*
domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
type:指定socket类型。常用的socket类型有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)等。
protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。
返回值:成功为文件描述符FD,失败为-1。
*/
e.g.
if (-1==(listend=socket(AF_INET, SOCK_STREAM, 0))){
perror("create listen socket error\n");
exit(1);
}
bind()
#include
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
sockfd:一般为调用socket函数返回的文件描述符。
*addr:地址的格式与协议有关,ipv4查看man 7 ip,ipv6查看man 7 ipv6。
addrlen:*addr的长度。
返回值:0为成功,-1为失败。
*/
e.g.
if (-1==bind(listend, (struct sockaddr*)&server, sizeof(struct sockaddr))){
perror("bind error\n");
exit(1);
}
listen()
#include
#include
int listen(int sockfd, int backlog);
/*
sockfd:一般为调用socket函数返回的文件描述符。
backlog:待连接的等待队列的最大值。
返回值:0为成功,-1为失败。
*/
e.g.
if (-1==listen(listend,5)){
perror("listen error\n");
exit(1);
}
accept()
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
sockfd:一般为调用socket函数返回的文件描述符。
*addr:存储客户端的信息(如ip和端口),传入一个空的结构体,便于函数内部为这个结构体赋值,这样外部就能访问了。
*addrlen:*addr的长度。
返回值:成功则返回与客户端建立连接的socket,也就是一个文件描述符FD,失败则返回-1。
*/
e.g.
if (-1==(connectd=accept(listend, (struct sockaddr*)&client, &len))){
perror("create connect socket error\n");
continue;
}
connect()
#include
#include
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
sockfd:一般为调用socket函数返回的文件描述符。
*addr:服务器端的地址,格式参考bind函数。
addrlen:*addr的长度。
返回值:0为成功,-1为失败。
*/
e.g.
if (0>(ret=connect(sockfd, (struct sockaddr*)&server, sizeof(struct sockaddr)))){
perror("connect error");
close(sockfd);
exit(1);
}
send()
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*
sockfd:文件描述符。
*buf:要发送的内容。
len:发送的内容的长度。
flags:标志位,默认为0
返回值:成功则返回发送的内容的长度,失败则返回-1
*/
e.g.
if ( 0 >send(connectd, send_buf, sendnum, 0)){
perror("send error\n");
close(connectd);
}
sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
/*
这个函数多了一个地址和地址的长度,用于发送UDP报文。
前面无需使用connect建立连接,将地址设置为NULL,长度设置为0,效果与send函数一样。
*/
recv()
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*
sockfd:文件描述符。
*buf:用来存放接收到的数据。
len:*buf的长度。
flags:标志位
返回值:成功则返回收到的内容的长度,失败则返回-1。
*/
e.g.
if (0>(recvnum = recv(connectd, recv_buf, sizeof(recv_buf), 0))){
perror("recv error\n");
close(connectd);
}
recv_buf[recvnum]='\0';
recvfrom()
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
/*
发送UDP数据
*/
close()
#include
int close(int sockfd);
/*
0 -OK;
-1-出错;
*/
UDP编程适用范围:
void *memset(void *str, int c, size_t n)
/*
复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
str -- 指向要填充的内存块。
c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
n -- 要被设置为该值的字符数
该值返回一个指向存储区 str 的指针
*/
e.g.
char send_buf[2048];
char recv_buf[2048];
struct sockaddr_in server;
memset(send_buf,0,2048);
memset(recv_buf,0,2048);
memset(&server,0,sizeof(struct sockaddr_in));
void *memcpy(void *str1, const void *str2, size_t n);
// 从存储区 str2 复制 n 个字节到存储区 str1
// 该函数返回一个指向目标存储区 str1 的指针
int opt;
opt = SO_REUSEADDR;
/*
一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用
SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用
server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项
TCP,先调用close()的一方会进入TIME_WAIT状态
*/
int setsockopt( int socket, int level, int option_name,const void *option_value, size_t option_len);
/*
socket:套接字描述符
level: 被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET
option_name: 指定准备设置的选项,option_name可以有哪些取值,这取决于level
(SO_DEBUG打开或关闭调试信息; SO_REUSEADDR打开或关闭地址复用功能; SO_DONTROUTE打开或关闭路由查找功能; SO_BROADCAST允许或禁止发送广播数据; SO_SNDBUF设置发送缓冲区的大小... )
*option_value: 指向存放选项值的缓冲区
option_len: 缓冲区的长度
*/
e.g.
int opt;
int listend;
opt = SO_REUSEADDR;
setsockopt(listend,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
将网络地址转换成“.”点隔的字符串格式
返回值:
若无错误发生,inet_ntoa()返回一个字符指针。否则的话,返回NVLL。
struct sockaddr_in client;
printf("the client ip addr is %s",inet_ntoa(client.sin_addr));
sendnum = sprintf(send_buf, "hello,the guest from %s\n", inet_ntoa(client.sin_addr));
将32位的主机字节顺序转化为32位的网络字节顺序
ip地址是32位的
将16位的主机字节顺序转化为16位的网络字节顺序
端口号是16位的
int atoi(const char *str);
/*
把参数 str 所指向的字符串转换为一个整数(类型为 int 型)
该函数返回转换后的长整数,如果没有执行有效的转换,则返回零。
*/
服务是由地址的端口号部分表示的。每个服务由一个唯一的、熟知的端口号来提供。此函数可以将一个服务名字映射到一个端口号。
该函数会/etc/services中匹配使用protocol proto 的service name,如果proto为NULL,则任何protocol将会被匹配
将一个端口号映射到一个服务名
void bzero(void *s, int n);
// 将参数s 所指的内存区域前n 个字节全部设为零
当我们面对如下问题时,SOCK_STREAM、SOCK_DGRAM 将显得这样无助:
原始套接字(SOCK_RAW)可以用来自行组装数据包,可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用。 原始套接字是基于IP 数据包的编程(SOCK_PACKET 是基于数据链路层的编程)。
另外,必须在管理员权限下才能使用原始套接字。
原始套接字(SOCK_RAW)与标准套接字(SOCK_STREAM、SOCK_DGRAM)的区别:
原始套接字直接置“根”于操作系统网络核心(Network Core);
SOCK_STREAM、SOCK_DGRAM 则“悬浮”于 TCP 和 UDP 协议的外围;
sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);
if(sockfd < 0){
perror("socket Error!\n");
exit(1);
}