文章目录
- 一、socket地址api
- 1.主机字节序和网络字节序
- 2.专用socket地址要强转成通用的sockaddr
- 3.IP地址转换函数
- 二、socket
- 三、数据读写
一、socket地址api
1.主机字节序和网络字节序
(1)大端字节序和小端字节序的区别
- 假设CPU的累加器一次能装载4字节(这里考虑32bit的机器)
- 大端字节序:
一个整数的高位字节(23-31bit)存储在内存的低地址,低位字节(0-7bit)存储在内存的高地址处 - 小端字节序:
整数的高位字节存储在内存的高地址出,低位字节存储在内存的低地址处
(2)主机字节序和网络字节序
- 现代PC大多采用小端字节序,因此小端字节序被称之为:主机字节序
- 大端字节序被称之未网络字节序
- 说明:即使是同一台机器上的两个进程(eg:一个C语言编写,一个JAVA编写,也要考虑字节序的问题,JAVA虚拟机采用大端字节序)
(3)判断机器字节序
#include <stdio.h>
void byteorder()
{
union
{
short value;
char union_bytes[sizeof(short)];
}test;
test.valu=0x0102;
if ((test.union_bytes[0]==1)&&(test.union_bytes[1]==2))
printf("大端字节序");
else if ((test.union_bytes[0]==2)&&(test.union_bytes[1]==1)
printf('小端字节序');
else
printf("i don not know");
}
2.专用socket地址要强转成通用的sockaddr
- 所有专用的socket的地址(以及sockaddr_storage)类型的变量要在实际使用时,都要转化为通用socket地址类型sockaddr(强制转换),因为所有的socket编程接口使用的地址参数类型都是sockaddr
(a)UNIX本地域使用的socket地址结构体是:struct sockaddr_un
(b)IPv4使用的socket地址结构体是:struct sockaddr_in,struct in_addr
(c)IPv6使用的socket地址结构体是:struct sockaddr_in6,struct in6_addr
3.IP地址转换函数
- (1)点分十进制的字符串表示的IPv4地址
- (2)网络字节序整数(二进制数)表示的Ipv4地址
#include<arpa/inet.h>
(1)转(2)的有:
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp, struct in_addr* inp);
int inet_pton(int af. const char*src, void* dst);
(2)转(1)的有:
char* inet_ntoa(struct in_addr in), const char* inet_ntop(int af, const void* src, char *dst, socklen_t cnt);
该函数是不可重入的,因为该函数的内部使用一个静态变量存储转化的结果,函数的返回值指向该静态内存。
eg:
char *szValue1=inet_ntoa("1.2.3.4");
char *szValue2=inet_ntoa("10.149.71.60");
printf("%s\n:", szValue1);
printf("%s\n:", szValue2);
得到的结果是:
10.194.71.60
10.194.71.60
二、socket
下面的都是系统调用
- 创建socket:它是可读,可写,可控制,可关闭的文件描述符
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
domain:底层协议族,PF_INET,用于IPv4,PF_INET6,用户IPv6;UNIX本地协议族,用PF_UNIX;
type:服务类型,SOCK_STREAM流服务,SOCK_UGRAM数据报服务;还可和SOCK_NONBLOCK,SOCK_CLOEXEC相与,
前者的意思是:将socket设为非阻塞的,后者的意思是用fork调用创建子进程时,在子进程中关闭该socket;
protocol参数:默认为0
- 命名socket:将一个socket与socket地址绑定称之为:给socket命名
#include <sys/types.h>
#inclue <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen)
bind是将my_addr所指的socket地址分配给未命名的sockfd文件描述符
在server程序中,通常需要命名socket,因为只有命名后,客户端才能知道该如何连接他;
在client程序中,通常不需要命名socket,而是采用匿名的方式,即:使用OS自动分配的socket地址
- 监听socket:创建一个监听队列来存放待处理的客户连接
#include <sys/socket.h>
int listen (int sockfd, int backlog)
socketfd指定被监听的socket;
backlog参数:提示内核监听队列的最大长度,监听队列的长度如果超过backlog,那么服务器将不再受理新的
客户连接,客户端也将收到ECONNREFFUSED错误信息;backlog参数指的是处于完全连接状态的socket的上线。
backlog参数的eg:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
static bool stop=false;
//SIGTERM信号的处理函数,触发的时候,结束主循环中的循环
static void handle_term(int sig)
{
stop=true;
}
int main(int argc, char* argv[])
{
signal(SIGTERM, handle_term);
if (argc<=3)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip=argv[1];
int port=atoi(argv[2]);
int backlog=atoi(argv[3]);
int sock=socket(PF_INET, SOCK_STREAM, 0);
assert(sock>=0);
//创建一个IPv4 socket地址
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port=htons(port);
int ret=bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret==-1);
ret=listen(sock, backlog);
assert(ret!=-1);
//循环等待连接,直到有SIGTERM信号将它中断
while (!stop)
{
sleep(1);
}
//关闭socket
close(sock);
return 0;
}
上面的代码最好用g++,以及gcc5.0以上的编译器进行编译,
$ ./a.out 192.168.1.109 12345 5
$ telnet 192.168.1.109 12345 & 在shell连续敲击即可
$ netstat -nt |grep 12345
可以发现处于
tcp 0 1 192.168.13.130:37322 192.168.1.109:12345 SYN_SENT
敲击几次有几次;
原作者所说的是:处于ESTABLISHED连接状态的只有6个(backlog值+1)
- 接受连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int socket, struct aockaddr *addr, socklen_t *addrlen);
sockfd参数:是执行过listen系统调用的监听socket;
addr参数:是用来获取被接受连接的远端socket的地址,该地址长度由addrlen参数指出
返回值:
成功时,返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被连接对应的客户端通信。
失败时,返回-1,并设置errno
注意:执行过listen,处于LISTEN状态的socket称之为:监听socket
处于ESTABLISHED状态的socket,称之为连接socket
eg:若监听队列中处于ESTABLISHED状态的连接对应的客户端处于网络异常(掉线或者提前退出等),那么服务器对
这个连接执行的accept调用是否成功?
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#define INET_ADDRSTRLEN 255
int main(int argc, char *argv[])
{
if (argc<=2)
{
printf("usage:%s ip_address port number\n", basename(argv[0]));
return 1;
}
const char* ip=argv[1];
int port=atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET, ip ,&address.sin_addr);
address.sin_port=htons(port);
int sock=socket(PF_INET, SOCK_STREAM, 0);
assert(sock>=0);
int ret=bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(sock>=0);
ret=listen(sock, 5);
assert(ret!=-1);
//暂停20s,以等待客户端连接和相关(掉线或退出)完成
sleep(20);
struct sockaddr_in client;
socklen_t client_addrlength=sizeof(client);
int connfd=accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd<0)
{
printf("errno is:%d\n", errno);
}
else
{
//接受连接成功,则打印出客户端的IP地址和端口号
char remote[INET_ADDRSTRLEN];
printf("connected with ip:%s and port: %d\n", inet_ntop(AF_INET, &client.sin_addr,remote,
INET_ADDRSTRLEN),ntohs(client.sin_port));
close(connfd);
}
close(sock);
return 0;
}
没执行成功。。。。可能与操作系统有关吧,但是编译啥的没问题,先按照书上的去理解吧
./testaccept 192.168.1.109 54321
telnet 192.168.1.109 54321
连接建立后(建立和断开连接的过程要在服务器启动后20s内完成),立马断开服务端,结果发现accept能够正常返回,输出如下:
connected with ip:192.168.1.109 and port:38545
然后再服务器上运行:
netstate -nt|grep 54321
tcp 0 0192.168.1.109:54321 192.168.1.109:38545 ESTABLISH
结论:
netstat命令的输出表明,accept调用对于客户端网络断开毫不知情。accept只是从监听队列中取出连接,而不论连接处于何种状态,
更不关心任何网络状况的变化
- 客户端需要发起系统调用connet主动与服务端建立连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd:由socket系统调用返回一个socket。
serv_addr:是服务器监听的socket地址。
addrlen:指定这个地址的长度
返回值:
成功返回0。sockfd唯一地标识该连接,客户端就可以通过读写sockfd来与服务器通信
失败返回-1,并设置errno
- 关闭文件描述符的系统调用
关闭一个连接其实就是关闭该连接对应的socket
#include <unistd.h>
int close(int fd);
fd:待关闭的socket
close并非立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才是真正的关闭连接。
在多进程程序中,fork会使父进程打开的socket的引用计数加1,so,我们必须在父子进程中都对该socket调用close才可以将连接关闭。
如果要立即终止连接,可以使用shutdown系统调用(专门为网络编程设计)
#include <sys/socket.h>
int shutdown(int sockfd, int howto)
socket:待关闭的socket
howto:决定了shutdown的行为
shutdown和close的区别是:
shutdown能分别关闭socket上的读or写or读写都关闭。
close在关闭连接时,只能将socket上的读和写同时关闭
三、数据读写
(1)tcp数据读写
- TCP流数据读写的系统调用
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv读取sockfd上的数据;
buf:缓冲区的位置
len:缓冲区的大小
flags:通常设置为0
返回值:
成功时,返回实际读取到的数据的长度,它可能小于期望的长度len。
出错时,返回-1,并设置errno
send往sockfd上写入数据
buf:缓冲区的位置
len:缓冲区的大小
返回值:
成功时,返回实际写入的数据的长度
失败时,返回-1,并设置errno
- eg:发送和接收带外数据
带外数据指的是:recv()和send的flags是MSG_OOB的情况
//发送带外数据的代码如下:
//接收带外数据的代码如下:
(2)udp数据读写
参考:<Linux 高性能服务器编程>