从 TCP/IP 协议说起
通信必然要求参与通信的双方约定好规则,这就是协议。当今世界的互联网基本就是跑在 TCP/IP 协议族上。两台主机通信最底层就是网卡对外发送的以太网帧。下面从报文结构的角度从底向上说说协议:
- 以太帧头, 以太头有本对端MAC地址,主要 用来二层交换, MAC地址是固化在网卡中的,一个主机可以插多个网卡。
- IP头, 有本对端IP地址,主要用来路由,一个网卡可以配多个IP。并且指定传输层协议类型,TCP协议号是6,UPD协议号是17。
这里是:IP协议号列表。
IP头:
- TCP头和UDP头, IP层会解出传输层的协议类型,然后加上端口号,就可以确定到应用程序进程。不同传输层协议当然可以有相同端口号。
TCP头:
- 应用层和进程功能紧密相关,如FTP, DNS等,知名服务往往有知名端口与之相对应,如的HTTP的80端口,当然也可以自己制定端口。
一些不错TCP/IP的资料
TCP协议要提供可靠连接,涉及了复杂的状态机,流控、拥塞控制、连接保活等方面,并且站在全网的角度,可以说是最复杂的协议。正式由于TCP在传输层提供了可靠的有连接通信,使其成为了使用最为广泛的协议。相比较之下,UDP就比较简单,至于连接的可靠性必然需要应用层来考虑了。
下面是一些不错的资料:
- TCP/IP详解 卷一、卷二、卷三
- UNIX网络编程卷1:套接字联网API
- UNIX网络编程卷2:进程间通信
- TCP 的那些事儿·上
- TCP 的那些事儿·下
- 了解 TCP 系统调用序列
- Berkeley套接字
- Linux man pages online API
Linux Socket TCP编程举例
server.c
/* server.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 1500
#define LISTEN_BACKLOG 10
#define BUFF_SIZE 32
int main()
{
struct sockaddr_in servsockaddr;
int listensockfd;
int connectsockfd;
char buff[BUFF_SIZE] = {0};
char *msg = "Welcome, client!\n";
size_t msglen = strlen(msg) + 1;
/* server向操作系统申请监听socket, 贯穿整个server生命周期,指定协议族,服务类型,传输协议号*/
listensockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (-1 == listensockfd) {
perror("Create socket failed");
exit(EXIT_FAILURE);
}
/* server 的本端IP与端口 */
servsockaddr.sin_family = AF_INET;
servsockaddr.sin_port = htons(SERV_PORT);
servsockaddr.sin_addr.s_addr = INADDR_ANY; //本机任意一个IP
/* server 绑定 监听socket 的本端地址: IP + 端口 */
if (-1 == bind(listensockfd,(const struct sockaddr *)&servsockaddr,
sizeof(servsockaddr))) {
perror("Server bind socket failed!");
close(listensockfd);
exit(EXIT_FAILURE);
}
/* server 的 监听socket 由主动置为被动的监听状态 */
if (-1 == listen(listensockfd, LISTEN_BACKLOG)) {
perror("Listen failed!");
close(listensockfd);
exit(EXIT_FAILURE);
}
/* 迭代处理每个client的连接, 未考虑并发能力,实际一般可以fork等进行并发处理*/
while (1) {
/* 接受连接, 返回连接socket, 并获取client的地址及地址的大小 */
connectsockfd = accept(listensockfd, NULL, NULL);
if (0 > connectsockfd) {
perror("Accetp failed!");
close(listensockfd);
exit(EXIT_FAILURE);
}
/* 收消息, 发消息, 网络IO操作,这里没判断返回值,还有其他网络IO API */
recv(connectsockfd, buff, sizeof(buff), 0);
printf("recv msg: %s", buff);
send(connectsockfd, msg, msglen, 0);
printf("send msg: %s", msg);
shutdown(connectsockfd, SHUT_RDWR);
close(connectsockfd);
}
close(listensockfd);
return 0;
}
client.c
/* client.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SERV_IP "127.0.0.1"
#define SERV_PORT 1500
#define BUFF_SIZE 32
int main()
{
struct sockaddr_in servsockaddr;
int clientsockfd;
char buff[BUFF_SIZE] = {0};
char *msg = "Hi, Server!\n";
size_t msglen = strlen(msg) + 1;
/* 向操作系统申请 socket,指定协议族,服务类型,传输层协议号 */
clientsockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (-1 == clientsockfd) {
perror("Create socket failed!");
exit(EXIT_FAILURE);
}
/* 填写服务器地址: IP + 端口 */
servsockaddr.sin_family = AF_INET;
servsockaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &servsockaddr.sin_addr); /* 可通过返回值判断IP地址合法性 */
/* client建链, TCP三次握手, 这里clientsockfd可以不显示绑定地址,内核会选client的本端IP并分配一个临时端口 */
if (-1 == connect(clientsockfd, (const struct sockaddr *)&servsockaddr, sizeof(servsockaddr))) {
perror("Connect failed!");
close(clientsockfd);
exit(EXIT_FAILURE);
}
/* 发消息, 收消息, 网络IO操作,这里没判断返回值,还有其他网络IO API */
send(clientsockfd, msg, msglen, 0);
printf("send msg: %s", msg);
recv(clientsockfd, buff, sizeof(buff), 0);
printf("recv msg: %s", buff);
/* 关闭TCP连接, 四次握手 */
shutdown(clientsockfd, SHUT_RDWR);
close(clientsockfd);
return 0;
}
Makefile
# Makefile
CFLAGS = -Werror -Wall -m64
COMPILE = $(CC) $^ $(CFLAGS) -o $@
.PHONY : all
all : server.bin client.bin
# 注意缩进都是 TAB
server.bin : server.c
$(COMPILE)
client.bin : client.c
$(COMPILE)
.PHONY : clean
clean :
rm *.bin
可以用 netstat -apn | grep "127.0.0.1"
查看上面例子中的连接状态。