Linux下的网络编程一般即是指socket套接字编程,入门比较矮简单,网上也有很多入门的例程。不过每次看过用过以后过段时间又忘了具体的操作了,又得去查,所以在这里总结整理一下,也省了以后查别人教程的时间。
socket套接字包含标准套接字(SOCK_STREAM,SOCK_DRAGM)以及原始套接字(SOCK_RAW),一般我们进行网络编程有标准套接字就够了,但如果要实现标准套接字(即TCP,UDP套接字)不能实现的功能,就需要用原始套接字了。这里还是主要总结一下标准套接字的用法。
如前所述,标准套接字分为TCP协议(SOCK_STREAM)和UDP协议(SOCK_DRAGM)两种type的工作流程,因为TCP是面向连接的服务,所以TCP网络编程会更复杂一点。不过不论是TCP还是UDP,其socket网络编程模式都是类似的,分为客户端和服务端。
一般在网络应用中,获取服务的客户即是客户端,提供服务的服务器即是服务端,不过也有些程序是互为服务和客户端,这种情况下, 一个程序既是客户端也是服务端。 以下两图描述了TCP和UDP socket通信的流程:
可以看到,因为TCP是面向连接的服务,包含三次握手建立连接的过程,所以TCP的服务器模式比UDP多了listen,accept函数,TCP客户端模式比UDP多了connect函数。
这张图将TCP数据交互的流程与socket函数作了一一对应,十分清楚,接下来就对其中的函数做一个整理介绍。
int socket(int family, int type, int protocol)
功能介绍:
在Linux操作系统中,一切皆文件,网络程序通过socket和其它几个函数的调用,会返回一个 通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处。。socket函数完成正确的操作是返回值大于0的文件描述符,当返回小于0的值时,操作错误。同样是返回一个文件描述符,但是会因为三个参数组合不同,对于数据具体的工作流程不同,对于应用层编程来说,这些也是不可见的。
参数说明:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
功能介绍:
bind函数主要应用于服务器模式一端,其主要的功能是将addrlen长度 struct sockaddr类型的myaddr地址与sockfd文件描述符绑定到一起,在sockaddr中主要包含服务器端的协议族类型,网络地址和端口号等。在客户端模式中不需要使用bind函数。当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。
参数说明:
typedef unsigned short sa_family_t;
struct in_addr {
__be32 s_addr;
};
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'。 */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
int listen(int sockfd, int backlog)
功能介绍:
刚开始理解listen函数会有一个误区,就是认为其操作是在等在一个新的connect的到来,其实不是这样的,真正等待connect的是accept操作,listen的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。listen和accept函数是服务器模式特有的函数,客户端不需要这个函数。当listen运行成功时,返回0;运行失败时,返回值位-1。
参数说明:
int accept(int sockfd, struct sockaddr *client_addr, socklen_t *len)
功能介绍:
接受函数accept其实并不是真正的接受,而是客户端向服务器端监听端口发起的连接。对于TCP来说,accept从阻塞状态返回的时候,已经完成了三次握手的操作。Accept其实是取了一个已经处于connected状态的连接,然后把对方的协议族,网络地址以及端口都存在了client_addr中,返回一个用于操作的新的文件描述符,该文件描述符表示客户端与服务器端的连接,通过对该文件描述符操作,可以向client端发送和接收数据。同时之前socket创建的sockfd,则继续监听有没有新的连接到达本地端口。返回大于0的文件描述符则表示accept成功,否则失败。
参数说明:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
功能介绍:
连接函数connect是属于client端的操作函数,其目的是向服务器端发送连接请求,这也是从客户端发起TCP三次握手请求的开始,服务器端的协议族,网络地址以及端口都会填充到connect函数的serv_addr地址当中。当connect返回0时说明已经connect成功,返回值是-1时,表示connect失败。
参数说明:
以上是TCP建立连接所需的五个函数(UDP所需函数也一样,只不过因为没有连接所以不需要listen,accept,connect函数)。在服务器端和客户端建立连接之后是进行数据间的发送和接收,TCP主要使用的接收函数是read或recv,发送函数是write或send;而UDP使用的则是recvfrom和sendto。
ssize_t write(int sockfd, const void *buf, size_t nbytes)
功能介绍:
write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1。 并设置errno变量。
参数说明:
ssize_t read(int sockfd, const void *buf, size_t nbytes)
功能介绍:
read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误。
参数说明:
int recv(int sockfd, void *buf, int len, int flags)
int send(int sockfd, void *buf, int len, int flags)
功能介绍:
recv和send函数提供了和read和write差不多的功能。不过它们提供 了第四个参数来控制读写操作。
参数说明:
前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合 (如果flags为0,则和read,write一样的操作。还有其它的几个选项,不过我们实际上用的很少)
MSG_DONTROUTE 不查找路由表
MSG_OOB 接受或者发送带外数据
MSG_PEEK 查看数据,并不从系统缓冲区移走数据
MSG_WAITALL 等待所有数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flag, const struct sockaddr *to, socklen_t tolen)
功能介绍:
sendto函数主要根据填充的接收方的地址信息向客户端或者服务器端发送数据,接收方的地址信息会提前设置在struct sockaddr类型的参数指针中,当返回值-1时,表明发送失败,当返回值大于等于0时,表示发送成功,并且发送数据的大小会通过返回值传递回来。
参数说明:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
功能介绍:
对于该函数主要的功能是,从客户端或者服务器端接收数据以及发送方的地址信息存储到本地的struct sockaddr类型参数变量当中,如果函数返回-1,所说明接收数据失败,如果返回的是大于等于0的值,则说明函数接收到的数据的大小。因为可以设置文件描述符的状态为阻塞模式,所以在没有接收到数据时,recvfrom会一直处于阻塞状态,直到有数据接收到。
参数说明:
数据的发送和接收结束以后需要关闭套接字,关闭套接字有两个函数close和shutdown。TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。针对不同的howto,系统回采取不同的关闭方式。
int close(int sockfd)
功能介绍:
当我们使用close时,会把读写通道都关闭。不过,close只会关闭本进程的 socket id,但连接还是开着的,用这个socket id的其他进程还能用这个连接进行读写。使用close中止一个连接,其实它只是减少文件描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭。
参数说明:
int shutdown(int sockfd, int howto)
功能介绍:
shutdown函数针对不同的howto,系统回采取不同的关闭方式,可以选择只中止一个方向的连接。与close不同的是,shutdown不考虑文件描述符的参考数,直接关闭文件描述符。在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用shutdown,那么所有的子进程都不能够操作了,这个时候若想只关闭其中一个子进程的套接字描述符我们只能够使用close。
参数说明:
server.c
====================================================================
#include // for sockaddr_in
#include // for socket
#include // for socket
#include // for printf
#include // for exit
#include // for bzero
/*
#include
#include
#include
#include
*/
#define HELLO_WORLD_SERVER_PORT 6666
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512
int main(int argc, char **argv)
{
//设置一个socket地址结构server_addr,代表服务器internet地址, 端口
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
//创建用于internet的流协议(TCP)socket,用server_socket代表服务器socket
int server_socket = socket(PF_INET,SOCK_STREAM,0);
if( server_socket < 0)
{
printf("Create Socket Failed!");
exit(1);
}
{
int opt =1;
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
}
//把socket和socket地址结构联系起来
if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)))
{
printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT);
exit(1);
}
//server_socket用于监听
if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) )
{
printf("Server Listen Failed!");
exit(1);
}
while (1) //服务器端要一直运行
{
//定义客户端的socket地址结构client_addr
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
//接受一个到server_socket代表的socket的一个连接
//如果没有连接请求,就等待到有连接请求--这是accept函数的特性
//accept函数返回一个新的socket,这个socket(new_server_socket)用于同连接到的客户的通信
//new_server_socket代表了服务器和客户端之间的一个通信通道
//accept函数把连接到的客户端信息填写到客户端的socket地址结构client_addr中
int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length);
if ( new_server_socket < 0)
{
printf("Server Accept Failed!\n");
break;
}
char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
length = recv(new_server_socket,buffer,BUFFER_SIZE,0);
if (length < 0)
{
printf("Server Recieve Data Failed!\n");
break;
}
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
strncpy(file_name, buffer, strlen(buffer)>FILE_NAME_MAX_SIZE?FILE_NAME_MAX_SIZE:strlen(buffer));
// int fp = open(file_name, O_RDONLY);
// if( fp < 0 )
printf("%s\n",file_name);
FILE * fp = fopen(file_name,"r");
if(NULL == fp )
{
printf("File:\t%s Not Found\n", file_name);
}
else
{
bzero(buffer, BUFFER_SIZE);
int file_block_length = 0;
// while( (file_block_length = read(fp,buffer,BUFFER_SIZE))>0)
while( (file_block_length = fread(buffer,sizeof(char),BUFFER_SIZE,fp))>0)
{
printf("file_block_length = %d\n",file_block_length);
//发送buffer中的字符串到new_server_socket,实际是给客户端
if(send(new_server_socket,buffer,file_block_length,0)<0)
{
printf("Send File:\t%s Failed\n", file_name);
break;
}
bzero(buffer, BUFFER_SIZE);
}
// close(fp);
fclose(fp);
printf("File:\t%s Transfer Finished\n",file_name);
}
//关闭与客户端的连接
close(new_server_socket);
}
//关闭监听用的socket
close(server_socket);
return 0;
}
client.c
====================================================================
#include // for sockaddr_in
#include // for socket
#include // for socket
#include // for printf
#include // for exit
#include // for bzero
/*
#include
#include
#include
#include
*/
#define HELLO_WORLD_SERVER_PORT 6666
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: ./%s ServerIPAddress\n",argv[0]);
exit(1);
}
//设置一个socket地址结构client_addr,代表客户机internet地址, 端口
struct sockaddr_in client_addr;
bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0
client_addr.sin_family = AF_INET; //internet协议族
client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址
client_addr.sin_port = htons(0); //0表示让系统自动分配一个空闲端口
//创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket
int client_socket = socket(AF_INET,SOCK_STREAM,0);
if( client_socket < 0)
{
printf("Create Socket Failed!\n");
exit(1);
}
//把客户机的socket和客户机的socket地址结构联系起来
if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr)))
{
printf("Client Bind Port Failed!\n");
exit(1);
}
//设置一个socket地址结构server_addr,代表服务器的internet地址, 端口
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
if(inet_aton(argv[1],&server_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数
{
printf("Server IP Address Error!\n");
exit(1);
}
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
socklen_t server_addr_length = sizeof(server_addr);
//向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接
if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0)
{
printf("Can Not Connect To %s!\n",argv[1]);
exit(1);
}
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
printf("Please Input File Name On Server:\t");
scanf("%s", file_name);
char buffer[BUFFER_SIZE];
bzero(buffer,BUFFER_SIZE);
strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));
//向服务器发送buffer中的数据
send(client_socket,buffer,BUFFER_SIZE,0);
// int fp = open(file_name, O_WRONLY|O_CREAT);
// if( fp < 0 )
FILE * fp = fopen(file_name,"w");
if(NULL == fp )
{
printf("File:\t%s Can Not Open To Write\n", file_name);
exit(1);
}
//从服务器接收数据到buffer中
bzero(buffer,BUFFER_SIZE);
int length = 0;
while( length = recv(client_socket,buffer,BUFFER_SIZE,0))
{
if(length < 0)
{
printf("Recieve Data From Server %s Failed!\n", argv[1]);
break;
}
// int write_length = write(fp, buffer,length);
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_lengthprintf("File:\t%s Write Failed\n", file_name);
break;
}
bzero(buffer,BUFFER_SIZE);
}
printf("Recieve File:\t %s From Server[%s] Finished\n",file_name, argv[1]);
close(fp);
//关闭socket
close(client_socket);
return 0;
}