一、字节序与大小端问题
网络使得数据可以从一个主机传递到另一个主机,不同的处理器在管理内存单元的数据时,对需要存放在多个内存单元的某一个数据的处理方式也不尽相同。所以这类数据的解释结果也不同,CPU 数据处理类型有大端和小端两种模式。
小端模式(Little-endian):操作数存放方式为高地址存放高字节;
大端模式(Big-endian):操作数的存放方式为高地址放低字节。X86平台采用小端模式,网络字节采用大端模式,而ARM处理器可同时支持大小端两种模式。如下是一个16bit 数据0x1234在小端模式和大端模式 CPU 中的存放方式。
我们可以利用共用体存放顺序的特点(即所有的成员都从低地址开始申请空间)来检查某个处理器是采用大端模式还是小端模式,示例程序如下:
#include <stdio.h>
#include <stdlib.h>
union word
{
int a;
char b;
}test;
int checkEndian()
{
test.a = 1;
return (1 == test.b);
}
int main()
{
if(checkEndian())
{
printf("This is little_endian\n");
}
else
{
printf("This is big_endian\n");
}
return 0;
}
为了统一,在网络编程时统一使用大端模式。所以在绑定 socket 的端口和 IP 地址时都要使用网络字节序,将网络字节序与主机字节序进行转换的函数有tonl()、htons()、ntohl()、ntohs()。
1、如果要发送纯字符串(单字节)给对方,就不需要特殊处理了,如
char buffer[] = "This is a string";
......
ret = send(socket_fd, buf, strlen(buf), 0);
......
2、如果要发送多个字节数据,如 short、int、float、double、long 等,则必须先转换为大端模式后再发送,如
int counter = 168;
......
counter = htonl(age);
......
ret = send(socket_fd, (void *)&age, sizeof(int), 0);
......
3、对于结构数据的传送就比较复杂了,如对于以下的结构体:
struct member {
char name[32];
int age;
char gender;
char address[128];
};
struct member studentInfo;
对 studentInfo 的各字段内容发送给对方可以采用以下方法:一是发送方和接收方都知道结构体 struct member 的定义,发送方可以这样写:
struct member studentInfo;
......
ret = send(socket_fd, studentInfo.name, 32, 0);
......
studentInfo.age = htonl(studentInfo.age);
ret = send(socket_fd, (void *)&studentInfo.age, sizeof(int), 0);
......
ret = send(socket_fd, &studentInfo.gender, sizeof(char), 0);
......
ret = send(socket_fd, studentInfo.address, 128, 0);
......
接收方则可以这样写
struct member studentInfo;
......
ret = recv(socket_fd, studentInfo.name, 32, 0);
......
ret = recv(socket_fd, (void *)&studentInfo.age, sizeof(int), 0);
studentInfo.age = ntohl(studentInfo.age);
......
ret = recv(socket_fd, &studentInfo.gender, sizeof(char), 0);
......
ret = recv(socket_fd, studentInfo.address, 128, 0);
......
另一种方法是对数据进行 pack 处理,发送方可以这样写
#pragma pack(1) //此行的参数不一定要写成1,关键是双方的定义一定要保持一致
struct member studentInfo;
......
studentInfo.age = htonl(studentInfo.age);
ret = send(socket_fd, (void *)&studentInfo, sizeof(struct member), 0);
......
接收方可以这样写
#pragma pack(1) //此行的参数不一定要写成1,关键是双方的定义一定要保持一致
struct member studentInfo;
......
ret = recv(socket_fd, (void *)&studentInfo, sizof(struct member), 0);
studentInfo.age = ntohl(studentInfo.age);
......
注意通信双方对齐方式的定义相同时才能用这种方法。
二、BSD Socket 网络通信编程
socket 是实现网络主机进程间通信的一种机制,从用户空间来看,socket 就是一个文件描述符,对socket 的操作和对普通文件的操作是相同的,即可以使用 read()、write()、close() 函数来操作。要向对方发送数据,只需要将数据 write 到socket 上;要接收数据,只需要阻塞地在socket 上读数据即可。从内核空间来看,socket 不再指向一个磁盘文件,相应的读写指针指向的代码亦是网卡驱动程序提供的数据发送和接收函数。其主要资源是一个内核空间的 struct sk_buff 结构体对象。在该对象中详细描述了通信双方的基本信息,缓冲的数据等。
socket 通信分为面向连接的数据流通信和面向无连接的数据报通信,两者最大的区别是面向连接的 TCP 通信需要通信双方建立可行的数据连接后才能进行通信;而面向无连接的 UDP 通信则只是简单的将数据发送到对应的目的主机即可,而不管对方的存活状态及包是否被正确传送和接收。
下图粗略地描述了面向连接的 socket 通信实现流程
下面实现一个简单的聊天程序,通信双方可以实时发送信息,并立即传送给对方。运行程序时,要通过命令行参数指明要连接的目标机的 IP 地址和要使用的端口,且执行 server 程序时还要加上缓冲队列的大小。如:
./server 192.168.1.124 7575 10 ./client 192.168.1.124 7575
它们的源码如下 :
client.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXBUFFER 1024
int main(int argc, char **argv)
{
int sockfd, len;
struct sockaddr_in dest;
char buffer[MAXBUFFER + 1];
if(argc != 3)
{
printf("Error format, it must be:\n\t\t%s IP port\n", argv[0]);
exit(EXIT_FAILURE);
}
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //创建 socket 对象
{
perror("Socket");
exit(errno);
}
printf("socket created\n");
bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET; //地址协议
dest.sin_port = htons(atoi(argv[2])); //地址端口
if(inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0) //对方 IP 地址
{
perror(argv[1]);
exit(errno);
}
if(connect(sockfd, (struct sockaddr *)&dest, sizeof(dest)) == -1) //发起连接
{
perror("Connet");
exit(errno);
}
printf("Server connected\n");
pid_t pid;
if(-1 == (pid = fork())) //创建子进程
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(0 == pid) //子进程用于接收数据
{
while(1)
{
bzero(buffer, MAXBUFFER + 1);
len = recv(sockfd, buffer, MAXBUFFER, 0);
if(len > 0)
{
printf("recv successful:'%s', %d byte recv\n", buffer, len);
}
else if(len < 0)
{
perror("recv");
break;
}
else
{
printf("the other one close, quit\n");
break;
}
}
}
else //父进程用于发送数据
{
while(1)
{
bzero(buffer, MAXBUFFER + 1);
printf("please input send message to send:");
fgets(buffer, MAXBUFFER, stdin);
if(!strncasecmp(buffer, "quit", 4))
{
printf("I will quit\n");
break;
}
len = send(sockfd, buffer, strlen(buffer) - 1, 0);
if(len < 0)
{
perror("send");
break;
}
}
}
close(sockfd);
return 0;
}
server.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXBUFFER 1024
int main(int argc, char *argv[])
{
int pid;
int sockfd, new_fd;
socklen_t len;
struct sockaddr_in my_addr, their_addr;
unsigned int myport, lisnum;
char buffer[MAXBUFFER + 1];
if(argv[2])
{
myport = atoi(argv[2]); //将命令行字符串转换为整数,用于端口
}
else
{
myport = 7575; //设置默认的端口
}
if(argv[2])
{
lisnum = atoi(argv[3]); //监听队列的大小
}
else
{
lisnum = 10;
}
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) //创建 socket 对象
{
perror("socket");
exit(EXIT_FAILURE);
}
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET; //地址协议
my_addr.sin_port = htons(myport); //地址端口
if(argv[1]) //把点分十进制字符串转换为网络顺序 IP 地址
{
my_addr.sin_addr.s_addr = inet_addr(argv[1]);
}
else
{
my_addr.sin_addr.s_addr = INADDR_ANY; //否则本机地址任意
}
if(bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) //绑定地址信息
{
perror("bind");
exit(EXIT_FAILURE);
}
if(listen(sockfd, lisnum) == -1) //监听网络
{
perror("listen");
exit(EXIT_FAILURE);
}
printf("wait for connet\n");
len = sizeof(struct sockaddr);
if((new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len)) == -1) //阻塞等待连接
{
perror("accept");
exit(EXIT_FAILURE);
}
else
{
printf("server: got connection from %s, port %d, socket %d\n",
inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
}
if(-1 == (pid = fork())) //创建新进程
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(0 == pid) //子进程用于发送消息
{
while(1)
{
bzero(buffer, MAXBUFFER + 1);
printf("input the message to send:");
fgets(buffer, MAXBUFFER, stdin);
if(!strncasecmp(buffer, "quit", 4))
{
printf("I will close the connet\n");
break;
}
len = send(new_fd, buffer, strlen(buffer) - 1, 0);
if(len < 0)
{
printf("message '%s' send failure! errno code is %d, errno message is '%s'\n",
buffer, errno, strerror(errno));
break;
}
}
}
else //父进程用于接收消息
{
while(1)
{
bzero(buffer, MAXBUFFER + 1);
len = recv(new_fd, buffer, MAXBUFFER, 0);
if(len > 0)
{
printf("Message recv successful: '%s', %d byte recv\n", buffer, len);
}
else if(len < 0)
{
printf("recv failure! errno code is %d, errno message is '%s'\n",
errno, strerror(errno));
break;
}
else
{
printf("The other one close quit\n");
break;
}
}
}
close(new_fd);
close(sockfd);
return 0;
}