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》