嵌入式 Linux網絡編程一――Socket網絡編程基礎
一、Socket簡介
1、網絡中進程間通信
本機進程使用進程號區別不同的進程進程間通信方式有管道、信號、消息隊列、共享內存、信號量等。網絡中進程間的通信首先需要識別進程所在主機在網絡中的唯一標識即網絡層的IP地址主機上的進程可以通過傳輸層的協議與端口號識別。
2、Socket原理
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層是一種編程接口。Socket屏蔽了不同網絡協議的差異支持面向連接(Transmission Control Protocol - TCPIP)和無連接(User Datagram Protocol-UDP 和 Inter-Network Packet Exchange-IPX)的傳輸協議。
二、Socket通信的基礎知識
1、網絡字節序
主機字節序即內存中存儲字節的方式分為大端序和小端序。何為大端、小端呢小端將低字節存儲在低地址。大端將高字節存儲在低字節。網絡中在處理多字節順序時一般采用大端序。在網絡傳輸時需要把主機字節序轉換到網絡字節序常用的轉換函數如下#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
2、
數據結構struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
/* Internet address. */
struct in_addr {
__be32 s_addr;
};
3、
IP地址轉換int inet_aton(const char *cp, struct in_addr *inp);
將cp所指的字符串IP地址轉換成32位的網絡字節序IP地址
in_addr_t inet_addr(const char *cp);
將cp所指的字符串IP地址轉換成32位的網絡字節序IP地址返回
char *inet_ntoa(struct in_addr in); 將32位網絡字節序IP地址轉換成點分十進制的字符串IP地址
4、地址結構使用
A、定義一個struct sockaddr_in類型的變量並清空
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
B、填充地址信息
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8080);
serveraddr.sin_addr.s_addr = inet_addr("192.168.6.100");
C、將該變量強制轉換為struct sockaddr類型在函數中使用
bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
5、TCP連接的建立
TCP協議通過三個報文段完成連接的建立建立連接的過程稱為三次握手(three-way handshake)。
第一次握手建立連接時客戶端發送syn包(syn=j)到服務器並進入SYN_SEND狀態等待服務器確認SYN同步序列編號(Synchronize Sequence Numbers)。
第二次握手服務器收到syn包必須確認客戶的SYNack=j+1同時自己也發送一個SYN包syn=k即SYN+ACK包此時服務器進入SYN_RECV狀態
第三次握手客戶端收到服務器的SYN+ACK包向服務器發送確認包ACK(ack=k+1)此包發送完畢客戶端和服務器進入ESTABLISHED狀態完成三次握手。
一個完整的三次握手是 請求---應答---再次確認。
當客戶端調用connect時觸發了連接請求向服務器發送了SYN J包這時connect進入阻塞狀態服務器監聽到連接請求即收到SYN J包調用accept函數接收請求向客戶端發送SYN K ACK J+1這時accept進入阻塞狀態客戶端收到服務器的SYN K ACK J+1之後這時connect返回並對SYN K進行確認服務器收到ACK K+1時accept返回至此三次握手完畢連接建立。
6、TCP連接的斷開
終止一個連接要經過四次握手簡稱四次握手釋放
TCP連接是全雙工的每個方向都必須單獨進行關閉。當一方完成數據發送任務後就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數據流動一個TCP連接在收到一個FIN後仍能發送數據。首先進行關閉的一方將執行主動關閉而另一方執行被動關閉。
A、客戶端A發送一個FIN用來關閉客戶A到服務器B的數據傳送B、服務器B收到這個FIN它發回一個ACK確認序號為收到的序號加1。和SYN一樣一個FIN將占用一個序號。 C、服務器B關閉與客戶端A的連接發送一個FIN給客戶端A。
D、客戶端A發回ACK報文確認並將確認序號設置為收到序號加1。
為什麼建立連接協議是三次握手而關閉連接卻是四次握手呢
因為服務端的LISTEN狀態下的SOCKET當收到SYN報文的建連請求後它可以把ACK和SYNACK起應答作用而SYN起同步作用放在一個報文裡來發送。但關閉連接時當收到對方的FIN報文通知時僅僅表示對方沒有數據發送給你了但你所有的數據未必都全部發送給對方了你未必會馬上會關閉SOCKET你可能還需要發送一些數據給對方之後再發送FIN報文給對方來表示你同意現在可以關閉連接了所以ACK報文和FIN報文多數情況下都是分開發送的。
為什麼TIME_WAIT狀態還需要等2MSL後才能返回到CLOSED狀態
雖然雙方都同意關閉連接了而且握手的4個報文也都協調和發送完畢按理可以直接回到CLOSED狀態就好比從SYN_SEND狀態到 ESTABLISH狀態那樣但是因為我們必須要假想網絡是不可靠的你無法保證你最後發送的ACK報文會一定被對方收到因此對方處於 LAST_ACK狀態下的SOCKET可能會因為超時未收到ACK報文而重發FIN報文所以這個TIME_WAIT狀態的作用就是用來重發可能丟失的 ACK報文。
7、getaddrinfo
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
node:一個主機名域名或者地址串(IPv4點分十進制串或者IPv6的16進制串)
service服務名可以是十進制的端口號可以是已定義服務名如ftp、http等
hints可以是一個空指針也可以是一個指向某個addrinfo結構體的指針調用者在這個結構中填入關於期望返回的信息類型的暗示。舉例來說如果指定的服務既支持TCP也支持UDP那麼調用者可以把hints結構中的ai_socktype成員設置成SOCK_DGRAM使得返回的僅僅是適用於數據報套接口的信息。
result本函數通過result指針參數返回一個指向addrinfo結構體鏈表的指針。
返回值0――成功非0――出錯struct addrinfo {
int ai_flags;
int ai_family;//AF_INET,AF_INET6或者AF_UNSPEC
int ai_socktype;//SOCK_STREAM or SOCK_DGRAM
int ai_protocol;//0
size_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
ai_flags:
AI_PASSIVE套接字地址用於監聽綁定
AI_CANONNAME需要一個規范名而不是別名
AI_V4MAPPED如果沒有找到IPV6地址返回映射到IPV6格式的IPV4地址
AI_ADDRCONFIG查詢配置的地址類型IPV4或IPV6
AI_NUMERICSERV以端口號返回服務
AI_NUMERICHOST以數字格式返回主機地址
gethostbyname函數僅支持IPV4
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
name主機名或域名
三、Socket接口函數
Socket編程的一般流程如下
1、socket
int socket(int domain, int type, int protocol);
創建一個Socket
domain即協議域又稱為協議族family。常用的協議族有AF_INET、AF_INET6、AF_LOCAL或稱AF_UNIXUnix域socket、AF_ROUTE等等。協議族決定了socket的地址類型在通信中必須采用對應的地址如AF_INET決定了要用ipv4地址32位的與端口號16位的的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
type指定socket類型。常用的socket類型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol指定協議。常用的協議有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議。當protocol為0時會自動選擇type類型對應的默認協議。
2、bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
把一個地址族中的特定地址賦給socket sockfd即socket描述字通過socket函數創建得到唯一標識一個socket。 addrconst struct sockaddr *指針指向要綁定給sockfd的協議地址。
IPV4的協議地址結構
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
IPV6的協議地址結構
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
3、listen
int listen(int sockfd, int backlog);
設置sockfd套接字為監聽套接字
sockfd參數即為要監聽的socket描述字
backlog參數為相應socket可以排隊的最大連接個數。
socket函數創建的socket默認是一個主動類型的listen函數將socket變為被動類型的等待客戶的連接請求。
4、accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);接收客戶端的請求建立連接套接字 參數sockfd是監聽套接字用來監聽一個端口
參數addr用來接收客戶端的協議地址可以設置為NULL。
參數len表示接收的客戶端的協議地址addr結構的大小的可設置為NULL
如果accept成功返回則服務器與客戶已經正確建立連接了服務器通過accept返回的套接字來完成與客戶的通信。
accept默認會阻塞進程直到有一個客戶連接建立後返回返回的是一個新可用的連接套接字。
監聽套接字: 在調用listen函數之後socket函數生成的主動連接的普通套接字就轉變為監聽套接字一般被accept函數調用的sockfd就是監聽套接字
連接套接字accept函數返回的是連接套接字代表與客戶端已經建立連接
一個服務器程序通常只創建一個監聽套接字在服務器程序的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個連接套接字當服務器完成了對某個客戶的服務相應的連接套接字就被關閉。
連接套接字並沒有占用新的端口與客戶端通信依然使用的是與監聽套接字sockfd一樣的端口號。
5、connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd參數即為客戶端的socket描述字addr參數為服務器的socket地址addrlen參數為socket地址的長度成功執行後客戶端通過調用connect函數來建立與TCP服務器的連接。
6、數據傳輸操作
ssize_t read(int fd, void *buf, size_t count);
read函數是負責從連接套接字fd中讀取內容。
fd參數是accept函數建立的連接套接字
buf參數是讀取的內容存放的內存緩沖區
count參數是要讀取的內容的大小
當讀成功時read返回實際所讀的字節數如果返回的值是0表示已經讀到文件的結束小於0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的如果是ECONNREST表示網絡連接出了問題。
ssize_t write(int fd, const void *buf, size_t count);
write函數是向連接套接字fd寫入內容
fd參數表示建立的連接套接字
buf參數表示要寫入內容所在的內存緩沖區
count參數表示要寫入的內容的大小
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send函數向連接套接字sockfd發送內容
sockfd參數表示發送到的連接套接字
buf參數表示要發送的內容所在的內存緩沖區
len參數表示要發送內容的長度
flags參數表示send的標識符一般為0
成功返回實際發送的字節數出錯返回-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv函數從連接套接字sockfd接收內容
sockfd參數表示從哪個連接套接字接收內容
buf參數表示接收的內容存放的內存緩沖區
len參數表示接收內容的實際字節數
flags參數表示recv操作標識符一般為0
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
sendmsg函數向連接套接字sockfd發送信息
sockfd參數表示向哪個連接套接字發送信息
msg參數表示要發送的信息的內存緩沖區
flags參數表示sendmsg函數操作的標識一般為0MSG_DONTWAIT表示非阻塞模式MSG_OOB表示發送帶外數據
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
socklen_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recvmsg函數從連接套接字scokfd接收信息
sockfd參數表示從哪個連接套接字接收信息
msg參數表示接收的信息存放的內存緩沖區
flags參數表示recvmsg函數操作的標識符
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sendto函數表示向連接套接字發送內容
sockfd參數表示向哪個連接套接字發送信息
buf參數表示發送的內容所在的內存緩沖區
len參數表示發送的信息的字節數
flags參數表示sendto函數的操作標識符
dest_addr參數表示發送到的地址的指針
addrlen參數表示發送到的地址的長度
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom函數從連接套接字sockfd接收信息
sockfd參數表示從哪個連接套接字接收信息
buf參數表示接收的信息存放的內存緩沖區
len參數表示接收的實際字節數
flags參數表示recvfrom函數的操作標識符
src_addr參數表示接收的信息來自的主機協議地址所存放的內存緩沖區
addrlen參數表示接收信息的源主機協議地址的長度
7、close
int close(int fd);
關閉斷開連接套接字
fd參數表示要斷開的連接套接字
四、程序實例
服務端server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define PORT 8888
#define LISTEN_QUEUE 10
#define BUFFER_SIZE 1024
int main()
{
///定義sockfd
int listenfd = socket(AF_INET,SOCK_STREAM, 0);
///定義sockaddr_in
struct sockaddr_in server_sockaddr;
bzero(&server_sockaddr, sizeof(server_sockaddr));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
///bind成功返回0出錯返回-1
if(bind(listenfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr))==-1)
{
fprintf(stderr, "bind function failed.\n");
exit(-1);
}
///listen成功返回0出錯返回-1
if(listen(listenfd,LISTEN_QUEUE) == -1)
{
fprintf(stderr, "listen function failed.\n");
exit(-1);
}
fprintf(stdout, "listening on %d\n", PORT);
///客戶端套接字
char recvbuf[BUFFER_SIZE];
char sendbuf[BUFFER_SIZE];
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
bzero(&client_addr, sizeof(client_addr));
///成功返回非負描述字出錯返回-1
int connsockfd = accept(listenfd, (struct sockaddr*)&client_addr, &length);
if(connsockfd<0)
{
fprintf(stderr, "connect function failed.\n");
exit(-1);
}
while(1)
{
bzero(recvbuf, sizeof(recvbuf));
bzero(sendbuf, sizeof(sendbuf));
int len = recvfrom(connsockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_addr, &len);
if(strcmp(recvbuf,"exit\n")==0)
break;
if(strcmp(recvbuf,"q\n")==0)
break;
if(strcmp(recvbuf,"quit\n")==0)
break;
fprintf(stdout, "have a new client:%s\n", inet_ntoa(client_addr.sin_addr));
fprintf(stdout, "message: %s\n", recvbuf);
strcpy(sendbuf, recvbuf);
send(connsockfd, sendbuf, len, 0);
}
close(connsockfd);
close(listenfd);
return 0;
}
客戶端client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main()
{
///定義sockfd
int clientsockfd = socket(AF_INET, SOCK_STREAM, 0);
///定義sockaddr_in
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT); ///服務器端口
servaddr.sin_addr.s_addr = inet_addr("192.168.0.200"); ///服務器ip
///連接服務器成功返回0錯誤返回-1
if (connect(clientsockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
fprintf(stderr, "connect function failed.\n");
exit(-1);
}
char sendbuf[BUFFER_SIZE];
char recvbuf[BUFFER_SIZE];
bzero(sendbuf, sizeof(sendbuf));
bzero(recvbuf, sizeof(recvbuf));
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
send(clientsockfd, sendbuf, strlen(sendbuf),0); ///發送
if(strcmp(sendbuf,"exit\n")==0)
break;
if(strcmp(sendbuf,"q\n")==0)
break;
if(strcmp(sendbuf,"quit\n")==0)
break;
recv(clientsockfd, recvbuf, sizeof(recvbuf),0); ///接收
fprintf(stdout, "%s\n", recvbuf);
bzero(sendbuf, sizeof(sendbuf));
bzero(recvbuf, sizeof(recvbuf));
}
close(clientsockfd);
return 0;
}
本文出自 “生命不息,奮斗不止” 博客,轉載請與作者聯系!