Linux的网络编程

本文详细介绍了Linux下的网络编程,包括socket的定义、地址信息、网络字节顺序转换函数、名字解析,以及socket的创建、使用、面向连接和无连接的socket操作。重点讲解了套接字模型、套接字选项和I/O控制,如SOL_SOCKET和SOL_IP级别的各种选项,以及select模型。内容涵盖了TCP和UDP的使用,以及套接字的非阻塞和阻塞模式等关键知识点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于多字节数据在内存中有两种存储方式:

Little-endian:低字节在前,高字节在后;

Big-endian:高字节在前,低字节在后

网络协议在处理多字节整数时,采用的是高端字节序,在编程时一定要考虑主机字节顺序与网络字节顺序的相互转换。

1      socket定义

socket是一种文件描述符。常用的socket类型有两种:流式socket(SOCK_STREAM)和数据报式socket(SOCK_DGRAM)。流式是一种面向连接的socket,针对于面向连接的TCP服务应用;数据报式socket是一种无连接的socket,对应于无连接的UDP服务应用。

1.1     socket地址信息

struct sockaddr类型是用来保存socket信息的:

structsockaddr

{

unsigned short sa_family;      /* 地址协议族,一般为AF_INET*/

char sa_data[14];                    /* 14字节的协议地址,包含该socket的IP地址和端口号 */

};

另外还有一种结构类型sockaddr_in:

structsockaddr_in

{

short int sin_family;                     /* 地址族 */

unsigned short int sin_port;        /* 网络字节顺序的端口号*/

struct in_addr sin_addr;              /* IP地址 */

unsigned char sin_zero[8];          /* 填充0 以保持与结构sockaddr同样大小 */

};

sockaddr_in结构使用更为方便,sockaddr_in.sin_zero应该用bzero( )或memset( )将其置为0。sockaddr_in的指针和sockaddr的指针可以通用。

structin_addr

{

unsigned long s_addr;            /* 网络字节顺序的IP地址*/

};

可以将s_addr指定为htonl( INADDR_ANY ),表示的是由系统自己指定IP地址。

在选择端口sin_port时,必须特别小心:

1.0~1023由IANA控制,是为固定服务保留的;

2.1024~49151是IANA列出来的、已注册的端口,供普通用户的普通用户进程或程序使用;

3.49152~65535是动态或私用端口,IANA这些端口上没有注册服务,可自由使用。

1.2     地址格式转换函数

#include<sys/socket.h>

#include<netinet/in.h>

#include<arpa/inet>

intinet_aton( const char *cp, struct in_addr *inp )     // 将点式IP转换成网络字节顺序的IP

char* inet_ntoa( struct in_addr in )                              // 将网络字节顺序的IP转换成点式IP

1.3     网络字节顺序转换函数

#include<netinet/in.h>

主机字节顺序---->网络字节顺序:

uint16_t htons(uint16_t hostshort);

uint32_t htonl(uint32_t hostlong);

网络字节顺序---->主机字节顺序:

uint16_t ntohs(uint16_t netshort);

uint32_t ntohl(uint32_t netlong);

1.4     名字解析

structhostent

{

char *   h_name;             //主机名

char ** h_aliases;            //主机别名数组

int          h_addrtype;       //主机地址类型

int          h_length;            //主机地址长度(字节)

char ** h_addr_list;        //主机地址数组,其中的地址是网络字节顺序。

}

 

#include<netdb.h>

structhostent *gethostbyname( const char *name )

参数name可以是主机名,也可以是点式IP地址。

 

#include<sys/socket>

structhostent *gethostbyaddr( const char *addr, int len, int type )

参数addr是指向主机地址的指针,该主机地址按照网络字节顺序。

参数len指定参数addr的字节长度。

参数type为AF_INET。

2      socket的使用

2.1     socket的创建

structprotoent

{

char *   p_name;      //协议名

char ** p_aliases;     //协议别名数组

int          p_proto;      //协议号

}

#include<netdb.h>

structprotoent *getprotoent( void )

 

#include<sys/types.h>

#include<sys/socket.h>

intsocket( int domain, int type, int protocol )

参数domain可以是PF_INET,表示IPv4地址家族;PF_INET6,表示IPv6地址家族。

参数type可以取SOCK_STREAM、SOCK_DGRAM或SOCK_RAW。

参数protocol可以是getprotoent()取回的任何一个协议号,一般取0表示IP。

2.2     面向连接的socket

2.2.1   服务器端函数

2.2.1.1      bind( )

#include<sys/types.h>

#include<sys/socket.h>

intbind( int sockfd,struct sockaddr *my_addr,intaddrlen )

2.2.1.2      listen( )

#include<sys/types.h>

#include<sys/socket.h>

intlisten( int sockfd,int backlog )

backlog指定在请求队列中允许的最大请求数,连接请求将在队列中等待accept( )它们。

2.2.1.3      accept( )

#include<sys/types.h>

#include<sys/socket.h>

intaccept(int sockfd,void *addr,int*addrlen)

addr通常是结构sockaddr_in的指针,用来存放发出连接请求的主机的信息。

addrten作为输入通常指定为sizeof(structsockaddr_in),addrten作为输出表示的是实际写入addr的字节长度。

accept( )将返回一个新的socket描述符,来供新连接使用。而服务器可以继续在原来的socket上进行监听,同时可以在新的socket描述符上进行数据send( )和recv( )操作。

2.2.2   客户端函数

2.2.2.1      connect( )

#include<sys/types.h>

#include<sys/socket.h>

intconnect( int sockfd,struct sockaddr *serv_addr,intaddrlen )

当参数s是SOCK_DGRAM类型时,addr指定的是在s上进行通信的对端主机的地址,可以对该类型的s绑定多次,来改变s所对应的通信对端的地址;

当参数s是SOCK_STREAM类型时,参数addr指定的是服务器地址,只能对该类型的s绑定一次。

2.2.3   数据传输函数

2.2.3.1      send( )

#include<sys/types.h>

#include<sys/socket.h>

ssize_tsend( int sockfd,const void *buf,size_tlen,int flags )

参数flags可以取:0、MSG_DONTROUTE、MSG_DONTWAIT或MSG_OOB。

2.2.3.2      recv( )

#include<sys/types.h>

#include<sys/socket.h>

ssize_trecv( int sockfd,void *buf,size_tlen,int flags )

参数flags可以取:0、MSG_PEEK、MSG_DONTWAIT或MSG_OOB。MSG_PEEK会将数据复制到接收端缓冲区内,但不从系统缓冲区中删除这些数据。

2.2.4   关闭连接

#include<sys/socket.h>

intshutdown( int sockfd, int how );

参数how可以取:SHUT_RD、SHUT_WR或SHUT_RDWR。

SHUT_RD表示不允许再对该套接字调用接收函数。

SHUT_WR表示不允许再对该套接字调用发送函数。

SHUT_RDWR表示取消收发操作。

 

#include<unistd.h>

close(sockfd);

shutdown( )并没有真正关闭该套接字,该套接字所占用的资源直到调用close( )才会被释放。

2.3     面向无连接的socket

先用socket( )建立套接字,再把新创建的套接字和网络接口bind( )。和面向连接的套接字不同的是,我们不必调用listen( )和accept( ),直接发送或接收数据:

2.3.1   发送端函数

2.3.1.1      sendto( )

#include<sys/types.h>

#include<sys/socket.h>

ssize_tsendto(   int sockfd,  const void *buf,  size_t len,  int flags,

const struct sockaddr *to,  int tolen);

2.3.2   接收端函数

2.3.2.1      recvfrom( )

#include<sys/types.h>

#include<sys/socket.h>

ssize_trecvfrom ( int sockfd,  void *buf,  size_t len, int flags,

struct sockaddr *from,  int *fromlen );

3      套接字模型

3.1     select模型

#include<sys/time.h>

#include<sys/types.h>

#include<sys/select.h>

#include<unistd.h>

intselect( int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,

structtimeval *timeout );

FD_CLR(int fd, fd_set *set )            :从set中删除fd

FD_ISSET(intfd, fd_set *set)            :检查fd是否是set的一名成员

FD_SET(intfd, fd_set *set)               :将fd加入set

FD_ZERO(fd_set *set)                      :将set初始化成空

参数numfds应该设成比三个fd_set中最大的fd还要大的一个值。

参数timeout指定select( )等待时间:

如果timeout是NULL,那么select( )会无限期地阻塞下去,直到有一个套接字符合条件后结束。

如果timeout是0,表明select( )会立即返回。

调用select( )之前,三个fd_set会设置成用户需要监听的各个fd,而在select( )成功返回之后,三个fd_set中只会保存符合条件的fd,而且函数返回值就是三个fd_set总共的fd数目。

如果超时,就返回0;

如果失败,就返回-1。

 

 

4      套接字选项和I/O控制

4.1     套接字选项

#include<sys/types.h>

#include<sys/socket.h>

intgetsockopt( int sockfd,  int level,  int optname, void* optval,  int* optlen );

intsetsockopt( int sockfd,  int level,  int optname, const void* optval,  int optlen );

4.1.1   SOL_SOCKET选项级别

4.1.1.1      SO_ACCEPTCONN

选项值类型

get/set

说明

integer bool

get

如果已通过listen( )将套接字置入监听模式,

这个选项就会返回TRUE。

SOCK_DGRAM类型的套接字不支持这一选项。

4.1.1.2      SO_BROADCAST

选项值类型

get/set

说明

integer bool

get/set

TRUE,套接字可以收发广播数据

SOCK_STREAM类型的套接字不支持这一选项。

要想接收一条广播消息,首先必须启用广播选项,然后使用接收函数recvfrom( )。另外一种方法是把套接字通过调用connect( )与广播地址连接起来,再调用recv( )来实现的。对UDP广播来说,必须指定一个端口号,以便向它发送数据报;接收端也必须请求在这个端口上接收广播数据。对UDP来说,存在着一个特殊的广播地址,所有广播数据均应发至该地址。这个地址便是255.255.255.255。

通常,只有在发送广播数据报时,才需要设置SO_BROADCAST选项。要想接收一个广播数据报,只需在指定的端口上,对进入的数据报进行监听即可。

4.1.1.3      SO_DONTROUTE

选项值类型

get/set

说明

bool

get/set

如果是TRUE,忽略路由表的存在,

通过套接字绑定的那个接口直接将数据传送出去。

如果是TRUE,等同于将参数flag设置为MSG_DONTROUTE,然后调用send( )。

4.1.1.4      SO_KEEPALIVE

选项值类型

get/set

说明

integer bool

get/set

如果是TRUE,套接字就会在会话过程中发送keepalive消息

SOCK_DGRAM类型的套接字不支持这一选项。

4.1.1.5      SO_LINGER

选项值类型

get/set

说明

struct linger

get/set

设置或获取当前的拖延时间

SOCK_DGRAM类型的套接字不支持这一选项。

SO_LINGER用于控制在执行close()或shutdown( )之后,套接字上正在排队的数据如何处理。

typedefstruct linger

{

u_short l_onoff;        // 是否linger

u_short l_linger;       // 单位是秒

}linger;

若l_onoff是一个非零值,l_linger指定一段拖延时间,此时的关闭方式称为从容关闭,close( )会在一个阻塞套接字上进入阻塞状态,直到剩余的数据全部发出或接收。如果超过规定的时间,所有尚未发送或接收的数据都会丢弃。

如果假如l_onoff是一个非零值,但l_linger为0,此时的关闭方式称为强行关闭,便不用担心close( )会进入阻塞状态,即使队列中的数据尚未发送或者尚未发出收到确认。

4.1.1.6      SO_RCVBUF

选项值类型

get/set

说明

int

get/set

用于返回或设置分配给套接字的接收缓冲区大小

4.1.1.7      SO_REUSEADDR

选项值类型

get/set

说明

integral bool

get/set

如果是TRUE,套接字就可以和正被使用的地址绑定到一起,

默认情况下,套接字不能和一个正在被使用的地址绑定到一起

4.1.1.8      SO_SNDBUF

选项值类型

get/set

说明

int

get/set

用于返回或设置分配给套接字的发送缓冲区大小

4.1.1.9      SO_TYPE

选项值类型

get/set

说明

int

get

返回指定套接字的类型

套接字类型包括SOCK_DGRAM、SOCK_STREAM和SOCK_RAW。

4.1.1.10  SO_SNDTIMEO

选项值类型

get/set

说明

struct timeval

get/set

获取或设置阻塞套接字上的数据发送超时时间

4.1.1.11  SO_RCVTIMEO

选项值类型

get/set

说明

struct timeval

get/set

获取或设置阻塞套接字上的数据接收超时时间

4.1.1.12  SO_OOBINLINE

选项值类型

get/set

说明

integer bool

get/set

如果是TRUE,

带外数据就会以内嵌方式在普通数据流中返回

SOCK_DGRAM类型的套接字不支持这一选项。

默认情况下,带外数据不是内嵌传送的,也就是说如果设置MSG_OOB标志调用一个接收函数,便会读取OOB数据。

如果设置了这个选项,OOB数据就会出现在一个接收函数返回的数据流中。

4.1.2   SOL_IP选项级别

4.1.2.1      IP_OPTIONS

选项值类型

get/set

说明

char[ ]

get/set

设置或获取IP头中的可选选项部分

 

设置一个IP选项时,传至setsockopt( )的数据采用如下的数据结构,最长可达40字节:

代码

长度

偏移

选项数据

1 byte

1 byte

1 byte

37 byte

代码字段指定IP选项类型。

长度字段指定IP选项头的长度。

偏移字段指定数据部分的起始偏移位置。

在下面的代码中,我们设置一个记录路由的选项。注意:我们定义的结构只占用了39字节。系统会自动进行填充,将其长度保持为一个字(32 bit)的整数倍。

struct ip_option_hdr

{

unsigned char      code;            //1个字节

unsigned char      length;          //1个字节

unsigned char      offset;           //1个字节

unsigned long      addrs[9];       //36个字节

} opthdr;

……;

memset( &opthdr, 0, sizeof(opthdr) );

opthdr.code=0x7;

opthdr.length=39;

opthdr.offset=4;

ret = setsockopt( s, SOL_IP, IP_OPTIONS,(char *)&opthdr, sizeof(opthdr) );

4.1.2.2      IP_HDRINCL

选项值类型

get/set

说明

bool

get/set

如果是TRUE,发送函数会将IP头包括在发送数据的前面,

所以接收函数获取的数据也包括IP头。

该选项只对SOCK_RAW有效,而且如果设定了该选项,则IP_ OPTIONS、IP_TOS、IP_TTL设定的值将被忽略。

 

4.1.2.3      IP_TOS

选项值类型

get/set

int

get/set

 

4.1.2.4      IP_TTL

选项值类型

get/set

int

get/set

 

4.1.2.5      IP_MULTICAST_IF

选项值类型

get/set

说明

struc ip_mreq (old)

或struc ip_mreqn (new)

get/set

获取或设置发送多播数据的网络接口

4.1.2.6      IP_MULTICAST_TTL

选项值类型

get/set

int

get/set

多播数据报采用的默认TTL设定是1

4.1.2.7      IP_MULTICAST_LOOP

选项值类型

get/set

说明

bool

get/set

如果是TRUE,多播数据将返回套接字

如果设为TRUE(默认值),在发送IP多播数据的时候,如果套接字是多播组的一名成员,数据便会返至套接字。如果设为FALSE,发出的多播数据不会返回至套接字。

4.1.2.8      IP_ADD_MEMBERSHIP

选项值类型

get/set

说明

struc ip_mreq (old)

或struc ip_mreqn (new)

set

将套接字加入一个指定的IP多播组

要想将套接字加入一个多播组,需要用socket( )来创建地址家族为AF_INET的一个套接字,同时将套接字的类型设为SOCK_DGRAM。

structip_mreg

{

struct in_addr    imr_multiaddr;

struct in_addr    imr_interface;

};

structip_mregn

{

struct in_addr    imr_multiaddr;         // IP multicast group address

struct in_addr    imr_interface;           // IP address of local interface

int                        imr_ifindex;              // interface index

};

 

4.1.2.9      IP_DROP_MEMBERSHIP

选项值类型

get/set

说明

struc ip_mreq (old)

或struc ip_mreqn (new)

set

将套接字从指定的IP组内删去

4.1.2.10  IP_MTU

选项值类型

get/set

说明

int

get

返回MTU,只有socket上的连接已经建立后才有效

4.2     标准I/O控制命令

4.2.1   ioctl( )

#include<ioctl.h>

intioctl( int sockfd,  int cmd,  void* argp );

4.2.1.1      SIOCGSTAMP

argp类型

输入

输出

struct timeval

用户收到的最后一个数据包的timestamp

4.2.1.2      SIOCINQ

argp类型

输入

输出

int

接收缓冲区队列中未被读取的数据长度

目标socket不能处于listen状态

 

4.2.1.3      SIOCOUTQ

argp类型

输入

输出

int

发送缓冲区队列中未被发送的数据长度

目标socket不能处于listen状态

4.2.1.4      SIOCATMARK

argp类型

输入

输出

int

判断已经接收完毕带外数据

如果设置参数flag为MSG_OOB标志调用一个接收函数,而且已经使用setsockopt( )设置SO_OOBINLINE,那么就会指出接下来是否准备接收OOB数据。

4.2.2   fcntl( )

#include<unistd.h>

#include<fcntl.h>

intfcntl( int fd, int cmd, long arg )

 

4.2.2.1      F_GETFL

可以获取fd的flags。

4.2.2.2      F_SETFL

只能修改如下的flag:O_APPEND、O_NONBLOCK、O_ASYNC、O_DIRECT。

新创建的socket默认都是阻塞类型的socket,可以通过fcntl()将socket的flag设置为O_NONBLOCK,从而将一个socket设置成为非阻塞类型的socket。

 

 

SO_RCVBUF is simpler to understand: it's the size of the buffer the kernel allocates to hold the data arriving into the given socket during the time between it arrives over the network and when it is read by the program that owns this socket. With TCP, if data arrives and you aren't reading it, the buffer will fill up, and the sender will be told to slow down (using TCP window adjustment mechanism). For UDP, once the buffer is full, new packets will just be discarded.

SO_SNDBUF, I think, only matters for TCP (in UDP, whatever you send goes directly out to the network). For TCP, you could fill the buffer either if the remote side isn't reading (so that remote buffer becomes full, then TCP communicates this fact to your kernel, and your kernel stops sending data, instead accumulating it in the local buffer until it fills up). Or it could fill up if there is a network problem, and the kernel isn't getting acknowledgements for the data it sends. It will then slow down sending data on the network until, eventually, the outgoing buffer fills up. If so, future write() calls to this socket by the application will block (or return EAGAIN if you've set the O_NONBLOCK option).


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值