TCP数据读写
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于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即可。recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
flags参数为数据收发提供了额外的控制,它可以取表5-4所示选项的一个或几个的逻辑或。
我们举例来说明如何使用这些选项。MSG_OOB选项给应用程序提供了发送和接收带外数据的方法。
testoobrecv->oobrecv.cpp
#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>
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( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
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
{
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;
}
在服务端(172.16.160.240)执行cmake ..
和make
,以及./testoobrecv 172.16.160.240 54321
testoobsend->oobsend.cpp
//发送带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
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 server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
printf( "send oob data out\n" );
const char* oob_data = "abc";
const char* normal_data = "123";
send( sockfd, normal_data, strlen( normal_data ), 0 );
send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
send( sockfd, normal_data, strlen( normal_data ), 0 );
}
close( sockfd );
return 0;
}
在客户端(172.16.160.250)执行cmake ..
和make
,以及./testoobsend 172.16.160.240 54321
服务端程序输出如下:
由此可见,客户端发送给服务器的3字节的带外数据“abe”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收(正如3.8节讨论的那样)。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的。
UDP数据读写
socket编程接口中用于UDP数据报读写的系统调用是:
#include <sys/socket.h>
#include <sys/types.h>
ssize_t recvfrom (int sockfd, void *buf, size_t len,
int flags, struct sockaddr* src_addr,
socklen_t *addr_len);
ssize_t sendto (int sockfd, const void *buf, size_t len,
int flags, const struct sockaddr* dest_addr,
socklen_t *addr_len);
recvfrom 读取 sockfd 上的数据,buf 和 len 参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr 所指的内容,addrlen 参数则指定该地址的长度。
sendto往sockfd上写入数据,buf 和 len 参数分别指定写缓冲区的位置和大小。dest_ addr 参数指定接收端的 socket 地址,addrlen 参数则指定该地址的长度。
这两个系统调用的fags参数以及返回值的含义均与send/recv系统调用的flags参数及返
回值相同。
值得一提的是,recvfrom/sendto 系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。
通用数据读写函数
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr * msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr * msg, int flags);
sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr结构体的定义如下:
struct msghdr
{
void * msg_name; //socket 地址
socklen_t msg_namelen; //socket 地址的长度
struct iovec * msg_iov; //分散的内存块
int msg_iovlen; //分散内存块的数量
void * msg_control; //指向辅助数据的起始位置
socklen_t msg_controllen; //指向辅助数据的起始位置
int msg_flags; //复制函数中的flags参数,并在调用过程中中更新
};
msg_name成员指向–个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。msg_namelen 成员则指定了msg_name 所指socket地址的长度。
msg_iov 成员是iovec结构体类型的指针,iovec 结构体的定义如下:
struct iovec
{
void * iov_base; //内存起始地址
size_t iov_len; //这块内存的长度
};
由上可见,iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec结构对象有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen 块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的传送。
msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
recvmsg/sendmsg的flags参数以及返回值的含义均与send/recv的flags参数及返回值相同。