1.socket地址API
主机字节序和网络字节序
字节序分为大端字节序和小端字节序
-
大端字节序 = 一个整数的高位字节存储在内存的低地址,低位字节存储在内存的高地址
通信的时候总采用大端字节序,也称为网络字节序
-
小端字节序 = 一个整数的低位字节存储在内存的高地址,低位字节存储在内存的低地址
PC一般采用小端字节序,所以也称为主机字节序
发送方总是把要发送的数据转化成大端字节序数据然后发送,而接受端知道对方传送过来的数据总是采用大端字节序,并根据自身采用的字节序决定是否对接收到的数据进行转换。
一个判断机器字节序的程序:
# include
void byteorder()
{
union
{
short value;
char union_bytes[ sizeof(short) ];
} test;
test.value = 0x0102;
if ( ( test.union_bytes[0] == 1 ) && ( test.union_bytes[1] == 2 ) )
{
printf( "big endian\n" );
}
else if ( ( test.union_bytes[0] == 2 ) && ( test.union_bytes[1] == 1 ) )
{
printf( "little endian\n" );
}
else
{
printf( "unknown...\n" )
}
}
linux提供了四个函数来完成主机字节序与网络字节序间的转换:
#include
//htonl = host to network long
unsigned long int htonl( unsigned long int hostlong );
unsigned short int htons( unsigned short int hostshort );
unsigned long int ntohl( unsigned long int netlong );
unsigned short int ntohs( unsigned short int netshort );
通用socket地址
用来表示socket地址的是结构体sockaddr,定义如下:
#include
struct sockaddr
{
//socket address family type,地址族类型
sa_family_t sa_family;
//14位包含了地址和端口
char sa_date[14];
}
地址族 = AF = address family
协议组 = PF = protocol family
专用socket地址
linux还为各个协议族提供了专门的socket地址结构体
IP地址转换函数
点分十进制转为网络字节序整数表示的IPV4地址:
#include
in_addr_t inet_addr( const char* strptr);
int inet_aton( const char* cp, struct in_addr* inp);
char* inet_ntoa( struct in_addr in );
2.创建socket
Linux中所有东西都是文件,socket是一种可读、可写、可控制、可关闭的文件描述符。
socket的创建:
#include
#include
int socket( int domain, int type, int protocol );
-
domain
:告诉系统使用哪个底层协议组TCP/IP:PF_INET(Protocol Family of Internet)(IPV4)
PF_INET6(IPV6)
Unix本地域协议族:PF_UNIX
-
type
:指定服务类型TCP流服务:SOCK_STREAM
UDP数据报服务:SOCK_UGRAM
-
protocol
:在前两个参数构成的协议集合下,再选择一个具体的协议大部分情况下都设为0
socket系统调用成功时返回一个socket描述符,失败则返回-1并设置errno
3.命名socket
通过 bind 来命名 socket:
#include
#include
int bind( int sockfd, const struct sockaddr* my_addr, socklen_tt addrlen );
bind 将my_addr所指的socket分配给未命名的sockfd文件描述符。
bind成功时返回0,失败则返回-1并设置errno。
4.监听socket
创建一个监听队列以存放待处理的客户连接:
#include
int listen( int sockfd, int backlog );
sockfd:指定被监听的socket
-
backlog:提示内核监听队列的最大长度
监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端将收到ECONNREFUSED错误信息
/* SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和
处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这
个信号. */
#include
#include
#include
#include
#include
#include
#include
#include
#include
//静态全局变量:定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见
static bool stop = 0;
/* SIGTERM信号的处理函数,触发时结束主程序中的循环 */
static void handle_term( int sig )
{
stop = true;
}
int main( int argc, char* argv[] )
{
signal( SIGTERM, handle_term);
if( argc <= 3)
{
//argv[0]是程序名
printf( "usage: %s ip_address port_number backlog\n", basename( argv[0] ) ); //basename:去除字符串中的目录部分
return 1;
}
//argv[1]是ip地址
const char* ip = argv[1];
//argv[2]是端口地址
//atoi = ascii to integer
int port = atoi( argv[2] );
//argv[3]是backlog
int backlog = atoi( argv[3] );
//创建一个socket,采用TCP/IP协议组,TCP流服务
int sock = socket( PF_INET, SOCK_STREAM, 0);
//如果socket没有创建成功则终止程序
assert( sock >= 0 );
/* 创建一个 IPV4 socket 地址 */
struct socketaddr_in address;
//置字节字符串前n个字节为零且包括‘\0’
//似乎是把address内容全部置0的意思
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
//讲ip地址由点分十进制串转换成网络字节序二进制值
//反过来就是ntop
inet_pton( AF_INET, ip, &address.sin_addr );
//将port主机序转网络序,host to net short
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;
}
argc 是 argument count的缩写,表示传入main函数的参数个数
argv 是 argument vector的缩写,表示传入main函数的参数序列或指针,并且第一个参数argv[0]一定是程序的名称,并且包含了程序所在的完整路径,所以确切的说需要我们输入的main函数的参数个数应该是argc-1个
struct的sizeof问题:
1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.
5.接受连接
从listen监听队列中接受一个连接:
#include
#include
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );
- sockfd:执行过listen系统调用的监听socket
- addr:被接受连接的远端socket地址
- addrlen:指出socket的地址长度
accept成功时,返回一个新的socket,该socket唯一地标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应的客户端通信;accept失败时,返回-1并设置errno
accept只是从监听队列中取出连接,而不论连接处于何种状态,也不关心任何网络状况的变化
6.发起连接
客户端通过如下系统调用来主动与服务器建立连接:
#include
#include
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );
- sockfd:socket系统调用返回的一个socket
- serv_addr:服务器监听的socket地址
connect成功时返回0,当连接成功建立,sockfd就唯一地标识了该连接,客户端可以通过读写sockfd来与服务器通信。
connect失败则返回-1,并设置errno,常见的两种是:
- ECONREFUSED:目标端口不存在,连接被拒绝
- ETIMEDOUT:连接超时
7.关闭连接
#include
int close( int fd );
fd:待关闭的socket
close系统调用并非总是关闭一个连接,而是将fd的引用计数 -1,当fd的引用计数为0时,才真的关闭连接。
多进程程序中,一次fork系统调用将使得父进程中打开的socket引用计数+1,因此我们必须在父进程和子进程中都对该socket执行close才能关闭连接。
如果要立即终止连接而不是将socket引用计数-1,可以使用如下的shutdown系统调用:
#include
int shutdown( int sockfd, int howto );