socket网络编程
1.1. 使用TCP协议的流程图
TCP通信的基本步骤如下:
服务端:socket---bind---listen---while(1){---accept---recv---send---close---}---close
客户端:socket----------------------------------connect---send---recv-----------------close
服务器端:
1. 头文件包含:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include <stdio.h>
#include <stdlib.h>
2. socket函数:生成一个套接口描述符。
原型:int socket(int domain,int type,int protocol);
参数:domainà{ AF_INET:Ipv4网络协议AF_INET6:IPv6网络协议}
typeà{tcp:SOCK_STREAM udp:SOCK_DGRAM}
protocolà指定socket所使用的传输协议编号。通常为0.
返回值:成功则返回套接口描述符,失败返回-1。
常用实例:int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}
3. bind函数:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。
原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
参数:sockfdà为前面socket的返回值。
my_addrà为结构体指针变量
对于不同的socket domain定义了一个通用的数据结构
struct sockaddr //此结构体不常用
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};
此sockaddr结构会因使用不同的socket domain而有不同结构定义,
例如使用AF_INET domain,其socketaddr结构定义便为
struct sockaddr_in //常用的结构体
{
unsigned short int sin_family; //即为sa_family èAF_INET
uint16_t sin_port; //为使用的port编号
struct in_addr sin_addr; //为IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
uint32_t s_addr;
};
addrlenàsockaddr的结构体长度。通常是计算sizeof(struct sockaddr);
返回值:成功则返回0,失败返回-1
常用实例:struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//或bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是大于1024的一个值。
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); //inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{perror("bind");close(sfd);exit(-1);}
(注:通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。)
4. listen函数:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
原型:int listen(int sockfd,int backlog);
参数:sockfdà为前面socket的返回值.即sfd
backlogà指定同时能处理的最大连接要求,通常为10或者5。最大值可设至128
返回值:成功则返回0,失败返回-1
常用实例:if(listen(sfd, 10) == -1)
{perror("listen");close(sfd);exit(-1);}
5. accept函数:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。(也就是说,类似于移动营业厅,如果有客户打电话给10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。对应过来,客户请求连接我们的服务器,我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)
原型:int accept(int s,struct sockaddr * addr,int * addrlen);
参数:sà为前面socket的返回值.即sfd
addrà为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
addrlenà表示结构体的长度,为整型指针
返回值:成功则返回新的socket处理代码new_fd,失败返回-1
常用实例:struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{perror("accept");close(sfd);exit(-1);}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
6. recv函数:用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:sockfdà为前面accept的返回值.即new_fd,也就是新的套接字。
bufà表示缓冲区
lenà表示缓冲区的长度
flagsà通常为0
返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
常用实例:char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{perror("recv");close(new_fd);close(sfd);exit(-1);}
puts(buf);
7. send函数:用新的套接字发送数据给指定的远端主机
原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:sà为前面accept的返回值.即new_fd
msgà一般为常量字符串
lenà表示长度
flagsà通常为0
返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1
常用实例:if(send(new_fd, "hello", 6, 0) == -1)
{perror("send");close(new_fd);close(sfd);exit(-1);}
8. close函数:当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源
原型:int close(int fd);
参数:fdà为前面的sfd,new_fd
返回值:若文件顺利关闭则返回0,发生错误时返回-1
常用实例:close(new_fd);
close(sfd);
客户端:
1. connect函数:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。
原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数:sockfdà为前面socket的返回值,即sfd
serv_addrà为结构体指针变量,存储着远程服务器的IP与端口号信息。
addrlenà表示结构体变量的长度
返回值:成功则返回0,失败返回-1
常用实例:struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{perror("connect");close(sfd);exit(-1);}
将上面的头文件以及各个函数中的代码全部拷贝就可以形成一个完整的例子,此处省略。
Example:将一些通用的代码全部封装起来,以后要用直接调用函数即可。如下:
通用网络封装代码头文件: tcp_net_socket.h
#ifndef __TCP__NET__SOCKET__H
#define __TCP__NET__SOCKET__H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
extern int tcp_init(const char* ip,int port);
extern int tcp_accept(int sfd);
extern int tcp_connect(const char* ip,int port);
extern void signalhandler(void);
#endif
具体的通用函数封装如下: tcp_net_socket.c
#include "tcp_net_socket.h"
int tcp_init(const char* ip, int port) //用于初始化操作
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//首先创建一个socket,向系统申请
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);//或INADDR_ANY
if(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将新的socket与制定的ip、port绑定
{
perror("bind");
close(sfd);
exit(-1);
}
if(listen(sfd, 10) == -1)//监听它,并设置其允许最大的连接数为10个
{
perror("listen");
close(sfd);
exit(-1);
}
return sfd;
}
int tcp_accept(int sfd) //用于服务端的接收
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
//sfd接受客户端连接,并创建新的socket为new_fd,将请求连接的客户端的ip、port保存在结构体clientaddr中
if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect...\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
return new_fd;
}
int tcp_connect(const char* ip, int port) //用于客户端的连接
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//向系统注册申请新的socket
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);
if(connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将sfd连接至制定的服务器网络地址serveraddr
{
perror("connect");
close(sfd);
exit(-1);
}
return sfd;
}
void signalhandler(void) //用于信号处理,让服务端在按下Ctrl+c或Ctrl+\的时候不会退出
{
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet,SIGINT);
sigaddset(&sigSet,SIGQUIT);
sigprocmask(SIG_BLOCK,&sigSet,NULL);
}
服务器端: tcp_net_server.c
#include "tcp_net_socket.h"
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./servertcp ip port\n");
exit(-1);
}
signalhandler();
int sfd = tcp_init(argv[1], atoi(argv[2])); //或int sfd = tcp_init("192.168.0.164", 8888);
while(1) //用while循环表示可以与多个客户端接收和发送,但仍是阻塞模式的
{
int cfd = tcp_accept(sfd);
char buf[512] = {0};
if(recv(cfd, buf, sizeof(buf), 0) == -1)//从cfd客户端接收数据存于buf中
{
perror("recv");
close(cfd);
close(sfd);
exit(-1);
}
puts(buf);
if(send(cfd, "hello world", 12, 0) == -1)//从buf中取向cfd客户端发送数据
{
perror("send");
close(cfd);
close(sfd);
exit(-1);
}
close(cfd);
}
close(sfd);
}
客户端: tcp_net_client.c
#include "tcp_net_socket.h"
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./clienttcp ip port\n");
exit(-1);
}
int sfd = tcp_connect(argv[1],atoi(argv[2]));
char buf[512] = {0};
send(sfd, "hello", 6, 0); //向sfd服务端发送数据
recv(sfd, buf, sizeof(buf), 0); //从sfd服务端接收数据
puts(buf);
close(sfd);
}
#gcc –o tcp_net_server tcp_net_server.c tcp_net_socket.c
#gcc –o tcp_net_client tcp_net_client.c tcp_net_socket.c
#./tcp_net_server 192.168.0.164 8888
#./tcp_net_client 192.168.0.164 8888
/* 备注
可以通过 gcc –fpic –c tcp_net_socket.c –o tcp_net_socket.o
gcc –shared tcp_net_socket.o –o libtcp_net_socket.so
cp lib*.so /lib //这样以后就可以直接使用该库了
cp tcp_net_socket.h /usr/include/ //这样头文件包含可以用include <tcp_net_socket.h>了
以后再用到的时候就可以直接用:
gcc –o main main.c –ltcp_net_socket //其中main.c要包含头文件 : include <tcp_net_socket.h>
./main
*/
注:上面的虽然可以实现多个客户端访问,但是仍然是阻塞模式(即一个客户访问的时候会阻塞不让另外的客户访问)。解决办法有:
1. 多进程(因为开销比较大,所以不常用)
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./servertcp ip port\n");
exit(-1);
}
int sfd = tcp_init(argv[1], atoi(argv[2]));
char buf[512] = {0};
while(1)
{
int cfd = tcp_accept(sfd);
if(fork() == 0)
{
recv(cfd,buf,sizeof(buf),0);
puts(buf);
send(cfd,"hello",6,0);
close(cfd);
}
else
{
close(cfd);
}
}
close(sfd);
}
2. 多线程
将服务器上文件的内容全部发给客户端
/* TCP 文件服务器演示代码 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <pthread.h>
#define DEFAULT_SVR_PORT 2828
#define FILE_MAX_LEN 64
char filename[FILE_MAX_LEN+1];
static void * handle_client(void * arg)
{
int sock = (int)arg;
char buff[1024];
int len ;
printf("begin send\n");
FILE* file = fopen(filename,"r");
if(file == NULL)
{
close(sock);
exit;
}
//发文件名
if(send(sock,filename,FILE_MAX_LEN,0) == -1)
{
perror("send file name\n");
goto EXIT_THREAD;
}
printf("begin send file %s....\n",filename);
//发文件内容
while(!feof(file))
{
len = fread(buff,1,sizeof(buff),file);
printf("server read %s,len %d\n",filename,len);
if(send(sock,buff,len,0) < 0)
{
perror("send file:");
goto EXIT_THREAD;
}
}
EXIT_THREAD:
if(file)
fclose(file);
close(sock);
}
int main(int argc,char * argv[])
{
int sockfd,new_fd;
//第1.定义两个ipv4 地址
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size,numbytes;
pthread_t cli_thread;
unsigned short port;
if(argc < 2)
{
printf("need a filename without path\n");
exit;
}
strncpy(filename,argv[1],FILE_MAX_LEN);
port = DEFAULT_SVR_PORT;
if(argc >= 3)
{
port = (unsigned short)atoi(argv[2]);
}
//第一步:建立TCP套接字 Socket
// AF_INET --> ip通讯
//SOCK_STREAM -->TCP
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(-1);
}
//第二步:设置侦听端口
//初始化结构体,并绑定2828端口
memset(&my_addr,0,sizeof(struct sockaddr));
//memset(&my_addr,0,sizeof(my_addr));
my_addr.sin_family = AF_INET; /* ipv4 */
my_addr.sin_port = htons(port); /* 设置侦听端口是 2828 , 用htons转成网络序*/
my_addr.sin_addr.s_addr = INADDR_ANY;/* INADDR_ANY来表示任意IP地址可能其通讯 */
//bzero(&(my_addr.sin_zero),8);
//第三步:绑定套接口,把socket队列与端口关联起来.
if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr)) == -1)
{
perror("bind");
goto EXIT_MAIN;
}
//第四步:开始在2828端口侦听,是否有客户端发来联接
if(listen(sockfd,10) == -1)
{
perror("listen");
goto EXIT_MAIN;
}
printf("#@ listen port %d\n",port);
//第五步:循环与客户端通讯
while(1)
{
sin_size = sizeof(struct sockaddr_in);
printf("server waiting...\n");
//如果有客户端建立连接,将产生一个全新的套接字 new_fd,专门用于跟这个客户端通信
if((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,&sin_size)) == -1)
{
perror("accept:");
goto EXIT_MAIN;
}
printf("---client (ip=%s:port=%d) request \n",inet_ntoa(their_addr.sin_addr),ntohs(their_addr.sin_port));
//生成一个线程来完成和客户端的会话,父进程继续监听
pthread_create(&cli_thread,NULL,handle_client,(void *)new_fd);
}
//第六步:关闭socket
EXIT_MAIN:
close(sockfd);
return 0;
}
/* TCP 文件接收客户端 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define FILE_MAX_LEN 64
#define DEFAULT_SVR_PORT 2828
main(int argc,char * argv[])
{
int sockfd,numbytes;
char buf[1024],filename[FILE_MAX_LEN+1];
char ip_addr[64];
struct hostent *he;
struct sockaddr_in their_addr;
int i = 0,len,total;
unsigned short port;
FILE * file = NULL;
if(argc <2)
{
printf("need a server ip \n");
exit;
}
strncpy(ip_addr,argv[1],sizeof(ip_addr));
port = DEFAULT_SVR_PORT;
if(argc >=3)
{
port = (unsigned short)atoi(argv[2]);
}
//做域名解析(DNS)
//he = gethostbyname(argv[1]);
//第一步:建立一个TCP套接字
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket");
exit(1);
}
//第二步:设置服务器地址和端口2828
memset(&their_addr,0,sizeof(their_addr));
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(port);
their_addr.sin_addr.s_addr = inet_addr(ip_addr);
//their_addr.sin_addr = *((struct in_addr *)he->h_addr);
//bzero(&(their_addr.sin_zero),8);
printf("connect server %s:%d\n",ip_addr,port);
/*第三步:用connect 和服务器建立连接 ,注意,这里没有使用本地端口,将由协议栈自动分配一个端口*/
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))==-1){
perror("connect");
exit(1);
}
if(send(sockfd,"hello",6,0)< 0)
{
perror("send ");
exit(1);
}
/* 接收文件名,为编程简单,假设前64字节固定是文件名,不足用0来增充 */
total = 0;
while(total< FILE_MAX_LEN){
/* 注意这里的接收buffer长度,始终是未接收文件名剩下的长度,*/
len = recv(sockfd,filename+total,(FILE_MAX_LEN - total),0);
if(len <= 0)
break;
total += len ;
}
/* 接收文件名出错 */
if(total != FILE_MAX_LEN){
perror("failure file name");
exit(-3);
}
printf("recv file %s.....\n",filename);
file = fopen(filename,"wb");
//file = fopen("/home/hxy/abc.txt","wb");
if(file == NULL)
{
printf("create file %s failure",filename);
perror("create:");
exit(-3);
}
//接收文件数据
printf("recv begin\n");
total = 0;
while(1)
{
len = recv(sockfd,buf,sizeof(buf),0);
if(len == -1)
break;
total += len;
//写入本地文件
fwrite(buf,1,len,file);
}
fclose(file);
printf("recv file %s success total lenght %d\n",filename,total);
//第六步:关闭socket
close(sockfd);
}
/* 备注读写大容量的文件时,通过下面的方法效率很高
ssize_t readn(int fd,char *buf,int size)//读大量内容
{
char *pbuf=buf;
int total ,nread;
for(total = 0; total < size; )
{
nread=read(fd,pbuf,size-total);
if(nread==0)
return total;
if(nread == -1)
{
if(errno == EINTR)
continue;
else
return -1;
}
total+= nread;
pbuf+=nread;
}
return total;
}
ssize_t writen(int fd, char *buf, int size)//写大量内容
{
char *pbuf=buf;
int total ,nwrite;
for(total = 0; total < size; )
{
nwrite=write(fd,pbuf,size-total);
if( nwrite <= 0 )
{
if( nwrite == -1 && errno == EINTR )
continue;
else
return -1;
}
total += nwrite;
pbuf += nwrite;
}
return total;
}
*/
3. 调用fcntl将sockfd设置为非阻塞模式。
#include <unistd.h>
#include <fcntl.h>
……
sockfd = socket(AF_INET,SOCK_STREAM,0);
iflags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd,F_SETFL,O_NONBLOCK | iflags);
……
4. 多路选择select
#include <sys/select.h>
#include "tcp_net_socket.h"
#define MAXCLIENT 10
main()
{
int sfd = tcp_init("192.168.0.164", 8888);
int fd = 0;
char buf[512] = {0};
fd_set rdset;
while(1)
{
FD_ZERO(&rdset);
FD_SET(sfd,&rdset);
if(select(MAXCLIENT + 1, &rdset, NULL, NULL, NULL) < 0)
continue;
for(fd = 0; fd < MAXCLIENT; fd++)
{
if(FD_ISSET(fd,&rdset))
{
if(fd == sfd)
{
int cfd = tcp_accept(sfd);
FD_SET(cfd,&rdset);
//……
}
else
{
bzero(buf, sizeof(buf));
recv(fd, buf, sizeof(buf), 0);
puts(buf);
send(fd, "java", 5, 0);
// FD_CLR(fd, &rdset);
close(fd);
}
}
}
}
close(sfd);
}
具体例子请参考《网络编程之select.doc》或《tcp_select》