一.Socket TCP/IP流程图
socket流程图
二.关键数据结构/结构体
sockaddr_in
在列出sockaddr_in结构体之前先将sockaddr结构体说明,此数据结构用作bind、connect、recvfrom、sendto等函数的参数,指明地址信息。sockaddr定义如下:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构sockaddr_in。
sockaddr_in结构体在<netinet/in.h>中定义,定义如下:
struct sockaddr_in{
short int sin_family;//协议族,在socket编程中只能是AF_INET
unsigned short int sin_port;//存储端口号(使用网络字节顺序)
struct in_addr sin_addr;//存储IP地址,使用in_addr这是数据结构
unsigned char sin_zero[8];//为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};
struct in_addr{//实际上就是32位ip地址
unsigned long s_addr;//按照网络字节顺序存储IP地址
};
typedef struct in_addr{
union{
struct{unsigned char s_b1,s_b2,s_b3,s_b4;} S_un_b;
struct{unsigned short s_w1,s_w2;} S_un_w;
unsigned long S_addr;
} S_un;
};
三.socket关键函数
1.socket()
socket()函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
参数说明:
- domain:协议族,对于socket要使用AF_INET
- type:套接字参数类型,设置为SOCK_STREAM或SOCK_DGRAM。一般使用SOCK_STREAM,因为它是基于TCP的,能保证数据正确传送到对方。SOCK_DGRAM是基于UDP的,无法保证数据正确传送到对方。如果大家使用UDP方式传输,要使用SOCK_DGRAM
- protocol:制定协议。常用协议有IPPROTO_TCP,IPPTOTO_UDP,IPPROTO_SCTP,IPPROTO_TIPC等,它们分别对应TCP,UDP,STCP,TIPC协议。TCP协议传入参数0即可
- 返回值:返回一个套接字描述符,出错返回-1
2.bind()
bind()函数原型:
int bind(int sock_fd,struct sockaddr_in *my_addr, int addrlen);
参数说明:
- sock_fd:套接字,传入的参数是socket()函数的返回值
- my_addr:一个指向包含有本机IP地址及端口号等信息得到sockaddr类的指针
- addrlen:地址长度,传入sizeof(my_addr)即可
- 返回值:成功返回0,失败返回-1;
3.connect()
connect()函数原型:
int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
参数说明:
- sock_fd:套接字,传入的参数是socket()函数的返回值
- serv_addr:包含远端主机IP地址和端口号的指针
- addrlen:同上,传入sizeof(serv_addr)即可
- 返回值:成功返回0,失败返回-1
4.listen()
listen()函数原型:
int listen(int sock_fd, int backlog);
参数说明:
- sock_fd:同上,传入的参数时socket()函数的返回值
- backlog:指定在请求队列中允许的最大请求数。进入的连接请求在使用系统调用accept()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。大多数系统的缺省设置为20
- 返回值:成功返回0,失败返回-1
5.accept()
accept()函数原型:
#include<sys/socket.h>
int accept(int sock_fd,(struct sockaddr*) addr,int* addrlen);
参数说明:
- sock_fd:同上,传入socket的返回值
- addr:指向sockaddr_in变量的指针
- addrlen:地址长度,传入&sizeof(addr)即可。注意:这个accept()函数传入的是&sizeof(addr)而不是sizeof(addr)!!!
- 返回值:成功返回新的套接字描述符,错误返回-1
6.write()
write()函数原型:
#include<unistd,h>
ssize_t write(int fd,const void *buf,size_t nbytes)
参数说明:
- fd:文件描述符,这里传入socket返回值
- buf:数据缓冲区
- nbytes:最大输出字节计数
- 返回值:返回实际写入的字节数,发生错误返回-1
7.read()
read()函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明:
- fd:文件描述符,这里传入socket返回值
- buf:数据缓冲区
- count:请求读取的字节数
- 返回值:返回实际读取到的字节数,返回0表示已到达文件尾或无数据可读,错误返回-1
8.send()
send()函数原型:
int send(int sockfd,const void* buf,int len,int flags);
参数说明:
- sockfd:套接字,传入socket()返回值
- buf:指向发送的数据的指针
- len:数据的字节长度
- flags:特殊传输标识,其值多为0。具体值可以参看博客http://183132459shp.blog.163.com/blog/static/43151635201212412117565/
- 返回值:返回实际发送的字节数,这可能比你实际想要发送的字节数少。如果返回的字节数比要发送的字节数少,你必须发送剩下的数据。出错返回-1
9.recv()
recv()函数原型:
int recv(int sockfd,void *buf,int len,int flags)
参数说明:
- sockfd:套接字,传入socket()的返回值
- buf:指向接收数据的指针
- len:数据的字节长度
- flags:特殊传输标志,其值多为0。具体值可以参看博客http://183132459shp.blog.163.com/blog/static/43151635201212412117565/
- 返回值:返回实际读取到缓冲区的字节数,出错返回-1
10.sendto()
sendto()函数原型:
int sendto(int sockfd,const void* buf,int len,unsigned int flags,const struct sockaddr* to,int tolen);
参数说明:
- sockfd:套接字,传入socket()的返回值
- buf:指向发送的数据的指针
- len:数据的字节长度
- flags:特殊传输标识,其值多为0
- to:指向包含目的IP地址和端口号的数据结构sockaddr的指针
- tolen:sizeof(struct sockaddr)
- 返回值:返回实际发送的字节数,如果出错则返回-1
11.recvfrom()
recvfrom()函数原型:
int recvfrom(int sockfd,void* buf,int len,unsigned int flags struct sockaddr* from,int* fromlen);
recvfrom参数说明:
- sockfd:套接字,传入socket()的返回值
- buf:指向接收数据的指针
- len:数据的字节长度
- flags:特殊传输标识,其值多为0
- from:指向本地计算机中包含源IP地址和端口号的数据结构sockaddr的指针
- tolen:sizeof(struct sockaddr)
- 返回值:返回接收到的字节数,如果出错则返回-1
12.close()
close()函数原型:
int close(sock_fd);
参数说明:
- sock_fd:套接字
- 返回值:成功返回0,失败返回-1
13.shutdown()
shutdown()可以拥有比close()更多的控制权。它允许你在某一个方向切断通信,或者切断双方的通信。shutdown()函数原型:
int shutdown(int sockfd,int how);
- sockfd:套接字
- how:你希望切断通信的标志位,可选参数如下:
- 0:Further receives are disallowed
- 1:Further sends are disallowed
- 2:Further sends and receivers are disallowed
- 成功返回0,失败返回-1
四.write-read,send-recv,sendto-recvfrom的选择
对于TCP/IP可以使用write-read和send-recv。使用send-recv功能上更优胜一些。具体可见博客http://blog.csdn.net/deng529828/article/details/6245254所述。本例中使用send-recv两个函数代替write-read。
五.代码!!!(源自http://www.oschina.net/code/snippet_97047_675)
服务器端:
//http://www.oschina.net/code/snippet_97047_675
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#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));
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 = 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);
}
fclose(fp);
printf("File:\t%s Transfer Finished\n",file_name);
}
//关闭与客户端的连接
close(new_server_socket);
}
//关闭监听用的socket
close(server_socket);
return 0;
}
说明:
htons将主机的无符号短整形转换成网络字节顺序。
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
setsockopt函数获取或者设置与某个套接字关联的选项,函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
参数说明:
- sock:套接字
- level:选项所在的协议层,可选参数如下:
- SOL_SOCKET: 基本套接口
- IPPROTO_IP: IPv4套接口
- IPPROTO_IPV6: IPv6套接口
- IPPROTO_TCP: TCP套接口
- optname:需要访问的选项名,对于level的选项,有不同的参数,具体配置可见http://blog.csdn.net/l_yangliu/article/details/7086256。本例中选择的时SQL_SOCKET中的SO_REUSERADDR选项代表允许重用本地地址和端口。
- optval:指向包含新选项值的缓冲
- optlen:选项的长度
- 返回值:成功返回0,失败返回-1
//http://www.oschina.net/code/snippet_97047_675
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#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);
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 = fwrite(buffer,sizeof(char),length,fp);
if (write_length<length)
{
printf("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;
}
说明:
inet_aton()用来将参数cp所指的网络地址字符串转换成网络使用的二进制的数字。函数原型如下:
int inet_aton(const char *cp, struct in_addr*addr);
参数说明:
- cp:包含ASCII表示的IP地址
- addr:存储新的ip的in_addr类型的地址。至于in_addr,本博客开头已经介绍。