在Linux 中的网络编程是通过socket 接口来进行的。人们常说的socket 是一种特殊的I/O 接口,它也是一种文件描述符。socket 是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket 也有一个类似于打开文件的函数调用,该函数返回一个整型的socket 描述符,随后的连接建立、数据传输等操作都是通过socket 来实现的。
socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,其中根据客户端还是服务端,或者根据使用TCP 协议还是UDP 协议,这些函数的调用流程都有所区别,这里先对每个函数进行说明,再给出各种情况下使用的流程图。
socket():该函数用于建立一个socket 连接,可指定socket 类型等信息。在建立了socket 连接之后,可对sockaddr 或sockaddr_in 结构进行初始化,以保存所建立的socket 地址信息。
bind():该函数是用于将本地IP 地址绑定到端口号,若绑定其他IP 地址则不能成功。另外,它主要用于TCP 的连接,而在UDP 的连接中则无必要。
listen():在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
accept():服务端程序调用listen()函数创建等待队列之后,调用accept()函数等待并接收客户端的连接请求。它通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。
connect():该函数在TCP 中是用于bind()的之后的client 端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。
send()和recv():这两个函数分别用于发送和接收数据,可以用在TCP 中,也可以用在UDP 中。当用在UDP 时,可以在connect()函数建立连接之后再用。
sendto()和recvfrom():这两个函数的作用与send()和recv()函数类似,也可以用在TCP 和UDP 中。当用在TCP 时,后面的几个与地址有关参数不起作用,函数作用等同于send()和recv();当用在UDP 时,可以用在之前没有使用connect()的情况下,这两个函数可以自动寻找指定地址并进行连接。
服务器端和客户端使用 TCP 协议的流程如图10.6 所示。
服务器端和客户端使用 UDP 协议的流程如图10.7 所示。
在socket的基础上,人们开发了许多应用层协议,比如http协议,ftp协议等等。这里我们以一种工业网络协议modbus协议为代表,介绍应用层网络协议的开发方法。
Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的Modbus协议:ModbusTCP。
Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。
标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。
ModbusTCP从机把主机当作一组寄存器,可以通过发送由{读写操作标志,地址,数据}组成的数据帧对主机进行操作。
一、以IO复用和多进程两种方式编写linux服务器程序
使用IO复用的linux服务器程序如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define SERV_PORT 8080
#define LIST 20 /*服务器最大接受连接 */
#define MAX_FD 10 /* FD_SET支持描述符数量 */
int main( int argc, char *argv[] )
{
int sockfd;
int err;
int i;
int connfd;
int fd_all[MAX_FD]; /* 保存所有描述符,用于select调用后,判断哪个可读 */
/*下面两个备份原因是select调用后,会发生变化,再次调用select前,需要重新赋值 */
fd_set fd_read; /* FD_SET数据备份 */
fd_set fd_select; /* 用于select */
struct timeval timeout; /* 超时时间备份 */
struct timeval timeout_select; /* 用于select */
struct sockaddr_in serv_addr; /*服务器地址 */
struct sockaddr_in cli_addr; /* 客户端地址 */
socklen_t serv_len;
socklen_t cli_len;
/* 超时时间设置 */
timeout.tv_sec = 10;
timeout.tv_usec = 0;
/* 创建TCP套接字 */
sockfd = socket( AF_INET, SOCK_STREAM, 0 );
if ( sockfd < 0 )
{
perror( "fail to socket" );
exit( 1 );
}
/* 配置本地地址 */
memset( &serv_addr, 0, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET; /* ipv4 */
serv_addr.sin_port = htons( SERV_PORT ); /* 端口, 8080 */
serv_addr.sin_addr.s_addr = htonl( INADDR_ANY ); /* ip */
serv_len = sizeof(serv_addr);
/* 绑定 */
err = bind( sockfd, (struct sockaddr *) &serv_addr, serv_len );
if ( err < 0 )
{
perror( "fail to bind" );
exit( 1 );
}
/* 监听 */
err = listen( sockfd, LIST );
if ( err < 0 )
{
perror( "fail to listen" );
exit( 1 );
}
/* 初始化fd_all数组 */
memset( fd_all, -1, sizeof(fd_all) );
fd_all[0] = sockfd; /* 第一个为监听套接字 */
FD_ZERO( &fd_read ); /* 清空 */
FD_SET( sockfd, &fd_read ); /* 将监听套接字加入fd_read */
int maxfd = fd_all[0]; /* 监听的最大套接字 */
while ( 1 )
{
/* 每次都需要重新赋值,fd_select,timeout_select每次都会变 */
fd_select = fd_read;
timeout_select = timeout;
/*
* 检测监听套接字是否可读,没有可读,此函数会阻塞
* 只要有客户连接,或断开连接,select()都会往下执行
*/
err = select( maxfd + 1, &fd_select, NULL, NULL, NULL );
/* err = select(maxfd+1, &fd_select, NULL, NULL, (struct timeval *)&timeout_select); */
if ( err < 0 )
{
perror( "fail to select" );
exit( 1 );
}
if ( err == 0 )
{
printf( "timeout\n" );
}
/* 检测监听套接字是否可读 */
if ( FD_ISSET( sockfd, &fd_select ) ) /* 可读,证明有新客户端连接服务器 */
{
cli_len = sizeof(cli_addr);
/* 取出已经完成的连接 */
connfd = accept( sockfd, (struct sockaddr *) &cli_addr, &cli_len );
if ( connfd < 0 )
{
perror( "fail to accept" );
exit( 1 );
}
/* 打印客户端的 ip 和端口 */
char cli_ip[INET_ADDRSTRLEN] = { 0 };
inet_ntop( AF_INET, &cli_addr.sin_addr, cli_ip, INET_ADDRSTRLEN );
printf( "----------------------------------------------\n" );
printf( "client ip=%s,port=%d\n", cli_ip, ntohs( cli_addr.sin_port ) );
/* 将新连接套接字加入 fd_all 及 fd_read */
for ( i = 0; i < MAX_FD; i++ )
{
if ( fd_all[i] != -1 )
{
continue;
}else{
fd_all[i] = connfd;
printf( "client fd_all[%d] join\n", i );
break;
}
}
FD_SET( connfd, &fd_read );
if ( maxfd < connfd )
{
maxfd = connfd; /* 更新maxfd */
}
}
/* 从1开始查看连接套接字是否可读,因为上面已经处理过0(sockfd) */
for ( i = 1; i < maxfd; i++ )
{
if ( FD_ISSET( fd_all[i], &fd_select ) )
{
printf( "fd_all[%d] is ok\n", i );
char buf[1024] = { 0 }; /* 读写缓冲区 */
int num = read( fd_all[i], buf, 1024 );
if ( num > 0 )
{
/* 收到 客户端数据并打印 */
printf( "receive buf from client fd_all[%d] is: %s\n", i, buf );
/* 回复客户端 */
num = write( fd_all[i], buf, num );
if ( num < 0 )
{
perror( "fail to write " );
exit( 1 );
}else{
/* printf("send reply\n"); */
}
}else if ( 0 == num ) /* 客户端断开时 */
{ /* 客户端退出,关闭套接字,并从监听集合清除 */
printf( "client:fd_all[%d] exit\n", i );
FD_CLR( fd_all[i], &fd_read );
close( fd_all[i] );
fd_all[i] = -1;
continue;
}
}else {
/* printf("no data\n"); */
}
}
}
return(0);
}
使用多进程的linux服务器程序如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <netinet/in.h>
/**
* TCP并发服务器,预先建立进程,同时到来的客户端分别由不同的进程并发处理
**/
#define PORT 8080
#define BUFFERSIZE 1024
#define PIDNUM 2
static void handle( int s )
{
int sc;
struct sockaddr_in client_addr; /* 客户端地址 */
char buffer[BUFFERSIZE];
int len;
int ret;
int size;
len = sizeof(client_addr);
time_t now;
/* 接收客户端的连接 */
while ( 1 )
{
memset( buffer, 0, BUFFERSIZE );
sc = accept( s, (struct sockaddr *) &client_addr, &len );
size = recv( sc, buffer, BUFFERSIZE, 0 );
if ( size > 0 && !strncmp( buffer, "TIME", 4 ) )
{
memset( buffer, 0, BUFFERSIZE );
now = time( NULL );
sprintf( buffer, "%24s\r\n", ctime( &now ) );
send( sc, buffer, strlen( buffer ), 0 ); /* 发送到客户端 */
}
close( sc );
}
}
int main( int argc, char*argv[] )
{
int s;
int ret;
struct sockaddr_in server_addr;
pid_t pid[PIDNUM];
int i;
s = socket( AF_INET, SOCK_STREAM, 0 );
if ( s < 0 )
{
perror( "socket error" );
return(-1);
}
/* 绑定地址到套接字 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons( PORT );
server_addr.sin_addr.s_addr = htonl( INADDR_ANY );
ret = bind( s, (struct sockaddr *) &server_addr, sizeof(server_addr) );
if ( ret < 0 )
{
perror( "bind error" );
return(-1);
}
ret = listen( s, 10 ); /* 监听 */
/* 建立子进程处理同时到来的客户端请求 */
for ( i = 0; i < PIDNUM; i++ )
{
pid[i] = fork();
if ( pid[i] == 0 )
{
handle( s );
}
}
while ( 1 )
;
close( s );
}
为了验证两个服务器程序是否可用,我们还需要编写一个客户端程序,客户端程序如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFERSIZE 1024
int main( int argc, char*argv[] )
{
int s;
int ret;
int size;
struct sockaddr_in server_addr;
char buffer[BUFFERSIZE];
s = socket( AF_INET, SOCK_STREAM, 0 );
if ( s < 0 )
{
perror( "socket error" );
return(-1);
}
bzero( &server_addr, sizeof(server_addr) );
/* 将地址结构绑定到套接字 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons( PORT );
server_addr.sin_addr.s_addr = htonl( INADDR_ANY );
/* 连接服务器 */
ret = connect( s, (struct sockaddr *) &server_addr, sizeof(server_addr) );
if ( ret == -1 )
{
perror( "connect error" );
return(-1);
}
memset( buffer, 0, BUFFERSIZE );
printf( “ \ nenter something: \ n ” );
scanf( “ % s ”, buffer );
size = send( s, buffer, strlen( buffer ), 0 );
if ( size < 0 )
{
perror( "send error" );
return(-1);
}
memset( buffer, 0, BUFFERSIZE );
size = recv( s, buffer, BUFFERSIZE, 0 );
if ( size < 0 )
{
perror( "recv error" );
return;
}
printf( "%s\n", buffer );
close( s );
return(0);
}