前面幾篇文章談到的關於TCP/IP應用層以下的協議,這些協議最終是在操作系統內核中實現的,套接字API是unix系統用於網絡連接的接口,後來被移植到windows系統中,就有了winsock。
TCP的Client/Server模式
在TCP/IP協議中已經講解了TCP協議中三次握手和四次握手過程,以及發送消息和接受消息。那麼在linux系統中,內核中已經將這些協議實現,現在我們一起看看linux下套接字編程的API。
TCP服務器端
1. 創建套接字
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非負描述字---成功 -1---失敗
第一個參數指明瞭協議簇,目前支持5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議);第二個參數指明套接口類型,有三種類型可選:SOCK_STREAM(字節流套接口)、SOCK_DGRAM(數據報套接口)和SOCK_RAW(原始套接口);如果套接口類型不是原始套接口,那麼第三個參數就爲0。
2.綁定套接字
把一個套接字地址(本機IP和端口號)綁定到創建的套接字上。綁定套接字時可以選擇指定IP地址和端口,也可以不指定。通配的IP地址用INADDR_ANY表示,通配的端口用0表示,通配的情況下由內核爲其指定相應的IP地址和端口號。
對於客戶端可以綁定套接字,但是一般不需要,因爲客戶端的端口號只是臨時的,由內核來分配更合理。但是對服務器而言,一般要使用知名端口號,如果不進行綁定,客戶端不知道目的端口號,連接不能完成。
通配地址實現:htonl(INADDR_ANY)
通配地址,內核將等到套接字已連接TCP或已經發出數據報(UDP)時才指定。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
返回:0---成功 -1---失敗
3.監聽
socket創建的套接字是主動套接字,調用listen後變成監聽套接字。TCP狀態有CLOSE躍遷到LISTEN狀態。
backlog是已完成隊列和未完成隊列大小之和,對於監聽套接字有兩個隊列,一個是未完成隊列,一個是已完成隊列。
- 未完成隊列:客戶端發送一個SYN包,服務器收到後變成SYN_RCVD狀態,這樣的套接字被加入到未完成隊列中。
- 已完成隊列:TCP已經完成了3次握手後,將這個套接字加入到已完成隊列,套接字處於ESTABLISHED狀態。
下圖中可以看出,TCP的三次握手是在調用connect函數時完成的,服務器端沒有調用函數,但是必須有套接字在某個端口監聽,不然會返回客戶端RST,終止連接。
#include<sys/socket.h>
int listen(int sockfd, int backlog);
調用listen函數後的套接字稱爲監聽套接字。
4.accept函數
accept函數從已完成連接的隊列中取走一個套接字,如果該隊列爲空,則accept函數阻塞。accept函數的返回值稱爲已連接套接字,已連接的套接字就建立一個完整的TCP連接,源IP地址,源端口號,目的IP地址,目的端口號都是唯一確定了。
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);
5.數據傳輸
- write和read函數:當服務器和客戶端的連接建立起來後,就可以進行數據傳輸了,服務器和客戶端用各自的套接字描述符進行讀/寫操作。因爲套接字描述符也是一種文件描述符,所以可以用文件讀/寫函數write()和read()進行接收和發送操作。
write()函數用於數據的發送
#include <unistd.h>
int write(int sockfd, char *buf, int len);
回:非負---成功 -1---失敗
參數sockfd是套接字描述符,對於服務器是accept()函數返回的已連接套接字描述符,對於客戶端是調用socket()函數返回的套接字描述符;參數buf是指向一個用於發送信息的數據緩衝區;len指明傳送數據緩衝區的大小。
read()函數用於數據的接收
#include <unistd.h>
int read(int sockfd, char *buf, intlen);
回:非負---成功 -1---失敗
參數sockfd是套接字描述符,對於服務器是accept()函數返回的已連接套接字描述符,對於客戶端是調用socket()函數返回的套接字描述符;參數buf是指向一個用於接收信息的數據緩衝區;len指明接收數據緩衝區的大小。
- send和recv函數:TCP套接字提供了send()和recv()函數,用來發送和接收操作。這兩個函數與write()和read()函數很相似,只是多了一個附加的參數。
(1)send()函數用於數據的發送。
#include <sys/types.h>
#include < sys/socket.h >
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
回:返回寫出的字節數---成功 -1---失敗
前3個參數與write()相同,參數flags是傳輸控制標誌。
(2)recv()函數用於數據的發送。
#include <sys/types.h>
#include < sys/socket.h >
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
回:返回讀入的字節數---成功 -1---失敗
前3個參數與read()相同,參數flags是傳輸控制標誌。
6.關閉套接字
close函數關閉套接字
#include <unistd.h>
int close(int sockfd);
TCP客戶端
1.創建套接字
2.連接服務器
TCP用connect函數來建立與TCP服務器的連接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
返回:0---成功 -1---失敗
客戶端發送的SYN包可能會遇到失敗,可能有以下幾種情況:
1. 如果客戶端沒有收到SYN的響應包,根據TCP的超時重發機制進行重發。75秒後還沒收到,就返回錯誤。
2. 如果目的主機沒有監聽目的端口號,就會返回一個RST的分節,客戶端收到RST後立刻返回錯誤。
3. 如果SYN在中間路由遇到目的不可達,客戶端收到ICMP報文,客戶端保存這個報文信息,並採用第一種情況方案解決,也就是重發。
3.收發數據
4.關閉套接字
TCP聊天室服務端程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 100
const char* IP = "127.0.0.1";
const unsigned int SERV_PORT = 7777;
void Chat(int sockfd);
int main(int argc, char *argv[])
{
int listenfd, connectfd;
struct sockaddr_in s_addr, c_addr;
char buf[BUFLEN];
unsigned int port, listnum;
pid_t childpid;
socklen_t len;
/*建立socket*/
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*設置服務器端口*/
port = SERV_PORT;
/*設置偵聽隊列長度*/
listnum = 5;
/*設置服務器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
// s_addr.sin_addr.s_addr = inet_aton(IP, &s_addr.sin_addr);
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/*把地址和端口幫定到套接字上*/
if((bind(listenfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
perror("bind");
exit(errno);
}
/*偵聽本地端口*/
if(listen(listenfd, listnum) == -1){
perror("listen");
exit(errno);
}
while(1){
printf("*****************server start***************\n");
len = sizeof(struct sockaddr);
if((connectfd = accept(listenfd, (struct sockaddr*) &c_addr, &len)) == -1){
perror("accept");
exit(errno);
}
else
{
printf("connected with client, IP is: %s, PORT is: %d\n", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));
}
//創建子進程
if((childpid = fork()) == 0)
{
Chat(connectfd);
/*關閉已連接套接字*/
close(connectfd);
/*是否退出服務器*/
printf("exit?:y->yes;n->no ");
bzero(buf, BUFLEN);
fgets(buf,BUFLEN, stdin);
if(!strncasecmp(buf,"y",1)){
printf("server stop\n");
break;
}
//退出子進程
exit(0);
}
}
/*關閉監聽的套接字*/
close(listenfd);
return 0;
}
void Chat(int sockfd)
{
socklen_t len;
char buf[BUFLEN];
while(1)
{
_retry:
/******發送消息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函數:從流中讀取BUFLEN-1個字符*/
fgets(buf,BUFLEN,stdin);
/*打印發送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4))
{
printf("server stop\n");
break;
}
/*如果輸入的字符串只有"\n",即回車,那麼請重新輸入*/
if(!strncmp(buf,"\n",1))
{
goto _retry;
}
/*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n'))
{
len = send(sockfd, buf,strlen(buf)-1,0);
}
/*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/
else
{
len = send(sockfd,buf,strlen(buf),0);
}
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else
{
if(len < 0 )
printf("receive failed\n");
else//服務器調用close函數後,系統阻塞函數調用,返回0
printf("client stop\n");
break;
}
}
}
TCP聊天室客戶端程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 100
const char* IP = "127.0.0.1";
const int SERV_PORT = 7777;
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in s_addr;
socklen_t len;
unsigned int port;
char buf[BUFLEN];
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*設置服務器端口*/
port = SERV_PORT;
/*設置服務器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if(inet_aton(IP, (struct in_addr*)&s_addr.sin_addr.s_addr) == 0){
perror("IP error");
exit(errno);
}
/*開始連接服務器*/
if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(errno);
}else
printf("*****************client start***************\n");
while(1){
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else{
if(len < 0 )
printf("receive failed\n");
else
printf("server stop\n");
break;
}
_retry:
/******發送消息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函數:從流中讀取BUFLEN-1個字符*/
fgets(buf,BUFLEN,stdin);
/*打印發送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("client stop\n");
break;
}
/*如果輸入的字符串只有"\n",即回車,那麼請重新輸入*/
if(!strncmp(buf,"\n",1)){
goto _retry;
}
/*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n'))
len = send(sockfd,buf,strlen(buf)-1,0);
/*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/
else
len = send(sockfd,buf,strlen(buf),0);
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
}
/*關閉連接*/
close(sockfd);
return 0;
}