分类: LINUX
2011-11-21 15:23:32
xgc94418297 的 Socket网络编程
一、什么是Socket
Socket(套接字)是BSD提供的网络应用编程界面(API),Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。现在它已经是网络编程中的标准。
Socket是一种特殊的进程间通信方式,不同机器上的进程都可以使用这种方式进行通信,网络中的数据传输是一种I/O操作。Socket也是一种文件描述符,它代表了一个通信管道的一个端点。read,write,close操作可应用于Socket描述符。在socket类型的文件描述符上,可以完成建立连接,数据传输等操作。常用的Socket类型有两种:
流式Socket-SOCK_STREAM,提供面向连接的Socket
数据报式Socket-SOCK_DGRAM,提供面向无连接的Socket
二、设置IP地址和端口号
字节序转换,字节序是指一个字节在存储器中是按高字节在前还是按低字节在前存放,分为大端格式和小端格式,异构计算机之间通信,需要转换自己的字节序为网络字节序。
字节序转换函数: (包含头文件:
uint32_t htonl() uint16_t htons()
以上返回网络字节序
uint32_t ntohl() uint16_t ntohs()
以上返回主机字节序
以下函数包含头文件
通用套接口地址结构体
struct sockaddr
{
sa_family_t sa_family;//地址族(无符号的短整数在linux中是两个字节)
char sa_data[14]; //地址数据
}
在使用其他套接口地址时要强制转换成通用套接口地址 (struct sockaddr *)&servAddr
IPv4套接字地址结构体
struct sockaddr_in
{
sa_family_t sin_family; 协议类型
in_port_t sin_port; 端口号
struct in_addr sin_addr; IP地址
unsigned char zero[8]; 为0
};
Ip地址结构体:
struct in_addr
{
in_addr_t s_addr;
};
struct in_addr以一个32位无符号数来表示,通常需要用到点分十进制数串与它之间的转换,使用inet_pton和inet_ntop来处理网络字节和主机字节之间的转换,原型如下:
int inet_pton(int family, const char *strptr, void *addrptr);
如果使用IPv4协议,第一个参数是AF_INET
第二个参数strprt为指向字符型的地址(ddd.ddd.ddd.ddd格式),函数将该地址转换为in_addr的结构体,并复制在*addrptr中。成功返回1,否则返回0。
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
这个函数作用和上面相反,多了一个参数len,它是所指向缓存区addrptr的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针。
有一个宏定义,指定了IP地址的长度为16,可以直接拿来用
#define INET_ADDRSTRLEN 16
以下函数包含头文件
三、建立Socket
创建套接字是进行任何网络通信时必须做的第一步,创建一个用于网络通信的I/O描述符(套接字),相当于在对文件读写前先用open获取文件描述符。
为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。Socket函数原型为:
int socket(int family, int type, int protocol);
family:协议族 常用的是AF_INET,代表的是IPv4协议,AF_INET6是IPv6协议
type:套接口类型 SOCK_STREAM SOCK_DGRAM
protocol:协议类别,一般为0就可以了。
调用socket函数返回一个int型的socket描述符。Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。
四、做为服务器
做为服务器需要具备三个条件:
1.具备一个可以确知的地址,以便别人能找到我
2.让操作系统知道你是一个服务器,而不是一个客户端
3.等待连接的到来
1.使用一个确知的端口来接收客户端的连接,需要使用bind函数将一个地址绑定到套接字,bind函数原型如下:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd:套接口描述字 myaddr:指向特定于协议的地址结构体指针
addrlen: 该地址结构的长度 返回值:0,成功;其他,失败
2.让套接字成为被动的,使用listen函数可以将套接口由主动修改为被动,listen函数原型如下:
int listen(int sockfd, int backlog);
sockfd: socket套接口描述字 backlog:连接队列的长度
返回值:0,成功;其他,失败
3.等待连接的到来,使用accept函数从连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(这个函数是阻塞的)。函数原型如下:
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
sockfd: socket套接口描述字 cliaddr: 客户端地址
addrlen:客户端地址结构体长度 返回值:已连接的套接口(代表当前连接)
五、做为客户端
客户端要知道服务哭的IP地址以及端口号,需要主动跟服务器建立连接,对于TCP协议,连接建立后才可以开始传输数据,使用connect函数建立连接,原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
addr: 服务器地址 addrlen:服务器地址结构体长度
返回值:0,成功;其他,错误。Connect函数建立连接后不会产生新的套接口
六、传输数据
当连接建立后,通信的两端便具备两个套接口,可以用read,write函数从这个通信管道读取后写入数据.
Send()和recv()这两个函数用于面向连接的socket上进行数据传输。
Send()函数原型为:
int send(int sockfd, const void *msg, int len, int flags);
Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0(关于该参数的用法可参照man手册)。
Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
recv()函数原型为:
int recv(int sockfd, void *buf, int len, unsigned int flags);
Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。
使用close函数关闭套接字,关闭一个代表已建立连接的套接字将导致另一端接收到一个0长度的数据包。服务器关闭socket创建的套接字导致服务器无法继续接受新的连接,蛤不会影响已经建立的连接。关闭accept返回的套接字将导致它所代表的连接被关闭,但不会影响服务器的监听。客户端用close,就是关闭连接。
你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
int shutdown(int sockfd, int how);
Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式:
·0-------不允许继续接收数据
·1-------不允许继续发送数据
·2-------不允许继续发送和接收数据,
·均为允许则调用close ()
shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno。
例子:
#include
#include
#include
#include
#include
#include
#include
//=============================================================
// 语法格式: void main(void)
// 实现功能: 主函数,建立一个TCP Echo Server
// 入口参数: 无
// 出口参数: 无
//=============================================================
int main(int argc, char *argv[])
{
char recvbuf[2048]; // 接收缓冲区
int sockfd; // 套接字
struct sockaddr_in servAddr; // 服务器地址结构体
unsigned short port = 8000; // 监听端口
if(argc > 1) // 由参数接收端口
{
port = atoi(argv[1]);
}
printf("TCP Server Started at port %d!\n", port);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建TCP套接字
if(sockfd < 0)
{
perror("Invalid socket");
exit(1);
}
bzero(&servAddr, sizeof(servAddr)); // 初始化服务器地址
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
printf("Binding server to port %d\n", port);
if(bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr)) != 0)
{
close(sockfd);
perror("binding err!");
exit(1);
}
if(listen(sockfd, 1) != 0)
{
close(sockfd);
perror("listen err!");
exit(1);
}
printf("waiting client...\n");
while(1)
{
char cliIP[INET_ADDRSTRLEN]; // 用于保存客户端IP地址
size_t recvLen;
struct sockaddr_in cliAddr; // 用于保存客户端地址
size_t cliAddrLen = sizeof(cliAddr);
// 必须初始化!!!
int connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);
// 获得一个已经建立的连接
if(connfd < 0)
{
close(sockfd);
perror("accept err!");
exit(1);
}
inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);
printf("client ip = %s\n", cliIP);
while((recvLen = read(connfd, recvbuf, 2048)) > 0)
{
write(connfd, recvbuf, recvLen);
}
close(connfd);
printf("client closed!\n");
}
close(sockfd);
return 0;
}
分析:
服务器:
unsigned short port = 8000;
定义端口号为8000
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
socket函数创建一个套接字,使用IPv4,TCP协议
要加上一个错误判断,如果socket返回值小于0,就是错误的
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr));
定义了服务器地址结构体,并初始化,bzero相当于memset,将结构体内容清0
给服务器地址赋值:
servAddr.sin_family = AF_INET; //IPv4
servAddr.sin_port = htons(port); //端口号,注意要把port转换为(16位)网络字节序
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,用INADDR_ANY指定为任意IP,转换成(32位)网络字节序
bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr))
绑定地址,要把服务器地址强制转换成sockaddr的结构体,同样加错误判断
listen(sockfd, 1)
开始监听,队列大小为1,只允许一个客户端访问,但实际在linux下不起作用
客户端:
char cliIP[INET_ADDRSTRLEN];
定义一个存放客户端IP地址的数组,大小为16
struct sockaddr_in cliAddr;
定义了客户端地址结构体
int connfd
connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);
客户端连接到服务器,获得一个已经建立的连接connfd,这个就是当前连接,对它进行读写操作
inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);
将客户端的IP地址转换成字符串,存放在cliIP中,INET_ADDSTRLEN指定大小为16
到这为止,连接已经建立完成,运行程序,用telnet登录服务器,输入服务器IP和端口号,连接成功,
就可以用read,write,读写connfd,实现通信。